From b3007a1b5d5740ac0957d5c6b6a1ded6c96c1eb1 Mon Sep 17 00:00:00 2001 From: Madeorsk Date: Mon, 25 Nov 2024 13:02:59 +0100 Subject: [PATCH] Load relations which are not inline. + Allow to retrieve metadata along with models. + Get all unloaded relations after model mapping. + Test hasMany relations. + Separate runtime and comptime relations instances. TODO: improve this. * Fix and improve relations query building. * Fix comptime select build called in runtime functions. --- src/insert.zig | 6 +- src/postgresql.zig | 18 +++-- src/query.zig | 33 ++++++-- src/relations.zig | 175 ++++++++++++++++++++++++++++++++++--------- src/repository.zig | 4 +- src/result.zig | 113 ++++++++++++++++++++++++---- src/sql.zig | 6 +- src/update.zig | 6 +- tests/relations.zig | 50 ++++++++++++- tests/repository.zig | 2 +- 10 files changed, 335 insertions(+), 78 deletions(-) diff --git a/src/insert.zig b/src/insert.zig index 0d9b4c0..c83fbc5 100644 --- a/src/insert.zig +++ b/src/insert.zig @@ -120,7 +120,7 @@ pub fn RepositoryInsert(comptime Model: type, comptime TableShape: type, comptim const Configuration = RepositoryInsertConfiguration(InsertShape); /// Result mapper type. - pub const ResultMapper = _result.ResultMapper(Model, TableShape, repositoryConfig, null, null); + pub const ResultMapper = _result.ResultMapper(Model, TableShape, null, repositoryConfig, null, null); arena: std.heap.ArenaAllocator, connector: database.Connector, @@ -336,8 +336,8 @@ pub fn RepositoryInsert(comptime Model: type, comptime TableShape: type, comptim defer queryResult.deinit(); // Map query results. - var postgresqlReader = postgresql.QueryResultReader(TableShape, null).init(queryResult); - return try ResultMapper.map(allocator, postgresqlReader.reader()); + var postgresqlReader = postgresql.QueryResultReader(TableShape, null, null).init(queryResult); + return try ResultMapper.map(false, allocator, self.connector, postgresqlReader.reader()); } /// Initialize a new repository insert query. diff --git a/src/postgresql.zig b/src/postgresql.zig index bb63ae7..2a5623d 100644 --- a/src/postgresql.zig +++ b/src/postgresql.zig @@ -162,8 +162,8 @@ pub fn makeMapper(comptime T: type, result: *pg.Result, allocator: std.mem.Alloc } /// PostgreSQL implementation of the query result reader. -pub fn QueryResultReader(comptime TableShape: type, comptime inlineRelations: ?[]const _relations.ModelRelation) type { - const InstanceInterface = _result.QueryResultReader(TableShape, inlineRelations).Instance; +pub fn QueryResultReader(comptime TableShape: type, comptime MetadataShape: ?type, comptime inlineRelations: ?[]const _relations.ModelRelation) type { + const InstanceInterface = _result.QueryResultReader(TableShape, MetadataShape, inlineRelations).Instance; // Build relations mappers container type. const RelationsMappersType = comptime typeBuilder: { @@ -215,9 +215,10 @@ pub fn QueryResultReader(comptime TableShape: type, comptime inlineRelations: ?[ pub const Instance = struct { /// Main object mapper. mainMapper: PgMapper(TableShape) = undefined, + metadataMapper: PgMapper(MetadataShape orelse struct {}) = undefined, relationsMappers: RelationsMappersType = undefined, - fn next(opaqueSelf: *anyopaque) !?_result.TableWithRelations(TableShape, inlineRelations) { + fn next(opaqueSelf: *anyopaque) !?_result.TableWithRelations(TableShape, MetadataShape, inlineRelations) { const self: *Instance = @ptrCast(@alignCast(opaqueSelf)); // Try to get the next row. @@ -227,7 +228,7 @@ pub fn QueryResultReader(comptime TableShape: type, comptime inlineRelations: ?[ const mainTable = try self.mainMapper.next(&row) orelse return null; // Initialize the result. - var result: _result.TableWithRelations(TableShape, inlineRelations) = undefined; + var result: _result.TableWithRelations(TableShape, MetadataShape, inlineRelations) = undefined; // Copy each basic table field. inline for (std.meta.fields(TableShape)) |field| { @@ -242,6 +243,10 @@ pub fn QueryResultReader(comptime TableShape: type, comptime inlineRelations: ?[ } } + if (MetadataShape) |_| { + result._zrm_metadata = (try self.metadataMapper.next(&row)).?; + } + return result; // Return built result. } @@ -266,6 +271,9 @@ pub fn QueryResultReader(comptime TableShape: type, comptime inlineRelations: ?[ fn initInstance(opaqueSelf: *anyopaque, allocator: std.mem.Allocator) !InstanceInterface { const self: *Self = @ptrCast(@alignCast(opaqueSelf)); self.instance.mainMapper = try makeMapper(TableShape, self.result, allocator, null); + if (MetadataShape) |MetadataType| { + self.instance.metadataMapper = try makeMapper(MetadataType, self.result, allocator, null); + } if (inlineRelations) |_inlineRelations| { // Initialize mapper for each relation. @@ -282,7 +290,7 @@ pub fn QueryResultReader(comptime TableShape: type, comptime inlineRelations: ?[ } /// Get the generic reader instance. - pub fn reader(self: *Self) _result.QueryResultReader(TableShape, inlineRelations) { + pub fn reader(self: *Self) _result.QueryResultReader(TableShape, MetadataShape, inlineRelations) { return .{ ._interface = .{ .instance = self, diff --git a/src/query.zig b/src/query.zig index 1a5836b..e092d4e 100644 --- a/src/query.zig +++ b/src/query.zig @@ -28,10 +28,12 @@ const CompiledRelations = struct { /// 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), comptime with: ?[]const relations.ModelRelation) type { +pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime repositoryConfig: repository.RepositoryConfiguration(Model, TableShape), comptime with: ?[]const relations.ModelRelation, comptime MetadataShape: ?type) type { const compiledRelations = comptime compile: { // Inline relations list. var inlineRelations: []relations.ModelRelation = &[0]relations.ModelRelation{}; + // Other relations list. + var otherRelations: []relations.ModelRelation = &[0]relations.ModelRelation{}; if (with) |_with| { // If there are relations to eager load, prepare their query. @@ -57,12 +59,15 @@ pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime 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)))); + } else { + // Add the current relation to other relations. + otherRelations = @ptrCast(@constCast(_comptime.append(otherRelations, relation))); } } break :compile CompiledRelations{ .inlineRelations = inlineRelations, - .otherRelations = &[0]relations.ModelRelation{}, + .otherRelations = otherRelations, .inlineSelect = if (inlineSelect.len > 0) ", " ++ _comptime.join(", ", inlineSelect) else "", .inlineJoins = if (inlineJoins.len > 0) " " ++ _comptime.join(" ", inlineJoins) else "", }; @@ -88,7 +93,7 @@ pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime const Self = @This(); /// Result mapper type. - pub const ResultMapper = _result.ResultMapper(Model, TableShape, repositoryConfig, compiledRelations.inlineRelations, compiledRelations.otherRelations); + pub const ResultMapper = _result.ResultMapper(Model, TableShape, MetadataShape, repositoryConfig, compiledRelations.inlineRelations, compiledRelations.otherRelations); arena: std.heap.ArenaAllocator, connector: database.Connector, @@ -285,8 +290,8 @@ pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime return result; } - /// Retrieve queried models. - pub fn get(self: *Self, allocator: std.mem.Allocator) !repository.RepositoryResult(Model) { + /// Generic queried models retrieval. + fn _get(self: *Self, allocator: std.mem.Allocator, comptime withMetadata: bool) !repository.RepositoryResult(if (withMetadata) _result.ModelWithMetadata(Model, MetadataShape) else Model) { // Build SQL query if it wasn't built. if (self.sql) |_| {} else { try self.buildSql(); } @@ -296,8 +301,22 @@ pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime defer queryResult.deinit(); // Map query results. - var postgresqlReader = postgresql.QueryResultReader(TableShape, compiledRelations.inlineRelations).init(queryResult); - return try ResultMapper.map(allocator, postgresqlReader.reader()); + var postgresqlReader = postgresql.QueryResultReader(TableShape, MetadataShape, compiledRelations.inlineRelations).init(queryResult); + return try ResultMapper.map(withMetadata, allocator, self.connector, postgresqlReader.reader()); + } + + /// Retrieve queried models. + pub fn get(self: *Self, allocator: std.mem.Allocator) !repository.RepositoryResult(Model) { + return self._get(allocator, false); + } + + /// Retrieved queries models with metadata. + pub fn getWithMetadata(self: *Self, allocator: std.mem.Allocator) !repository.RepositoryResult(_result.ModelWithMetadata(Model, MetadataShape)) { + if (MetadataShape) |_| { + return self._get(allocator, true); + } else { + unreachable; + } } /// Initialize a new repository query. diff --git a/src/relations.zig b/src/relations.zig index 0bc1223..71147bc 100644 --- a/src/relations.zig +++ b/src/relations.zig @@ -1,5 +1,6 @@ const std = @import("std"); const pg = @import("pg"); +const _database = @import("database.zig"); const _sql = @import("sql.zig"); const repository = @import("repository.zig"); const _query = @import("query.zig"); @@ -62,7 +63,9 @@ pub fn typedMany( }; const FromKeyType = std.meta.fields(FromModel)[std.meta.fieldIndex(FromModel, fromRepositoryConfig.key[0]).?].type; - const QueryType = _query.RepositoryQuery(ToModel, ToTable, toRepositoryConfig, null); + const QueryType = _query.RepositoryQuery(ToModel, ToTable, toRepositoryConfig, null, struct { + __zrm_relation_key: FromKeyType, + }); return struct { const Self = @This(); @@ -79,15 +82,29 @@ pub fn typedMany( unreachable; // No possible join in a many relation. } - fn genSelect(_: *anyopaque, comptime table: []const u8, comptime prefix: []const u8) []const u8 { + fn _genSelect(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 { - var models: []const FromModel = undefined; - models.len = opaqueModels.len; - models.ptr = @ptrCast(@alignCast(opaqueModels.ptr)); - const query: *QueryType = @ptrCast(@alignCast(opaqueQuery)); + fn genSelect(_: *anyopaque, comptime table: []const u8, comptime prefix: []const u8) []const u8 { + return _genSelect(table, prefix); + } + + fn getQueryType() type { + return QueryType; + } + + fn buildQuery(_: *anyopaque, prefix: []const u8, opaqueModels: []const *anyopaque, allocator: std.mem.Allocator, connector: _database.Connector) !*anyopaque { + const models: []const *FromModel = @ptrCast(@alignCast(opaqueModels)); + + // Initialize the query to build. + const query: *QueryType = try allocator.create(QueryType); + errdefer allocator.destroy(query); + query.* = QueryType.init(allocator, connector, .{}); + errdefer query.deinit(); + + // Build base SELECT. + const baseSelect = comptime _genSelect(toRepositoryConfig.table, ""); // Prepare given models IDs. const modelsIds = try query.arena.allocator().alloc(FromKeyType, models.len); @@ -97,20 +114,34 @@ pub fn typedMany( switch (config) { .direct => { - // Build WHERE condition. - try query.whereIn(FromKeyType, "\"" ++ toRepositoryConfig.table ++ "\".\"" ++ foreignKey ++ "\"", modelsIds); - }, - .through => |through| { - query.join(.{ - .sql = "INNER JOIN \"" ++ through.table ++ "\" ON " ++ - "\"" ++ toRepositoryConfig.table ++ "\"." ++ modelKey ++ " = " ++ "\"" ++ through.table ++ "\"." ++ through.joinModelKey, + // Add SELECT. + query.select(.{ + .sql = baseSelect ++ ", \"" ++ toRepositoryConfig.table ++ "\".\"" ++ foreignKey ++ "\" AS \"__zrm_relation_key\"", .params = &[0]_sql.RawQueryParameter{}, }); // Build WHERE condition. - try query.whereIn(FromKeyType, "\"" ++ through.table ++ "\".\"" ++ through.joinForeignKey ++ "\"", modelsIds); + try query.whereIn(FromKeyType, "\"" ++ toRepositoryConfig.table ++ "\".\"" ++ foreignKey ++ "\"", modelsIds); + }, + .through => |through| { + // Add SELECT. + query.select(.{ + .sql = try std.fmt.allocPrint(query.arena.allocator(), baseSelect ++ ", \"{s}pivot" ++ "\".\"" ++ through.joinForeignKey ++ "\" AS \"__zrm_relation_key\"", .{prefix}), + .params = &[0]_sql.RawQueryParameter{}, + }); + + query.join(.{ + .sql = try std.fmt.allocPrint(query.arena.allocator(), "INNER JOIN \"" ++ through.table ++ "\" ON AS \"{s}pivot" ++ "\" " ++ + "\"" ++ toRepositoryConfig.table ++ "\"." ++ modelKey ++ " = " ++ "\"{s}pivot" ++ "\"." ++ through.joinModelKey, .{prefix, prefix}), + .params = &[0]_sql.RawQueryParameter{}, + }); + + // Build WHERE condition. + try query.whereIn(FromKeyType, try std.fmt.allocPrint(query.arena.allocator(), "\"{s}pivot" ++ "\".\"" ++ through.joinForeignKey ++ "\"", .{prefix}), modelsIds); }, } + + return query; // Return built query. } pub fn relation(self: *Self) Relation(ToModel, ToTable) { @@ -122,6 +153,15 @@ pub fn typedMany( .inlineMapping = inlineMapping, .genJoin = genJoin, .genSelect = genSelect, + .getQueryType = getQueryType, + }, + }; + } + + pub fn runtimeRelation(self: *Self) RuntimeRelation { + return .{ + ._interface = .{ + .instance = self, .buildQuery = buildQuery, }, }; @@ -185,7 +225,9 @@ 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, null); + const QueryType = _query.RepositoryQuery(ToModel, ToTable, toRepositoryConfig, null, struct { + __zrm_relation_key: FromKeyType, + }); // Get foreign key from relation config or repository config. const foreignKey = switch (config) { @@ -233,15 +275,29 @@ fn typedOne( }; } - fn genSelect(_: *anyopaque, comptime table: []const u8, comptime prefix: []const u8) []const u8 { + fn _genSelect(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 { - var models: []const FromModel = undefined; - models.len = opaqueModels.len; - models.ptr = @ptrCast(@alignCast(opaqueModels.ptr)); - const query: *QueryType = @ptrCast(@alignCast(opaqueQuery)); + fn genSelect(_: *anyopaque, comptime table: []const u8, comptime prefix: []const u8) []const u8 { + return _genSelect(table, prefix); + } + + fn getQueryType() type { + return QueryType; + } + + fn buildQuery(_: *anyopaque, prefix: []const u8, opaqueModels: []const *anyopaque, allocator: std.mem.Allocator, connector: _database.Connector) !*anyopaque { + const models: []const *FromModel = @ptrCast(@alignCast(opaqueModels)); + + // Initialize the query to build. + const query: *QueryType = try allocator.create(QueryType); + errdefer allocator.destroy(query); + query.* = QueryType.init(allocator, connector, .{}); + errdefer query.deinit(); + + // Build base SELECT. + const baseSelect = comptime _genSelect(toRepositoryConfig.table, ""); // Prepare given models IDs. const modelsIds = try query.arena.allocator().alloc(FromKeyType, models.len); @@ -251,8 +307,15 @@ fn typedOne( switch (config) { .direct => { + // Add SELECT. + query.select(.{ + .sql = baseSelect ++ ", \"" ++ fromRepositoryConfig.table ++ "\".\"" ++ fromRepositoryConfig.key[0] ++ "\" AS \"__zrm_relation_key\"", + .params = &[0]_sql.RawQueryParameter{}, + }); + query.join((_sql.RawQuery{ - .sql = "INNER JOIN \"" ++ fromRepositoryConfig.table ++ "\" ON \"" ++ toRepositoryConfig.table ++ "\"." ++ modelKey ++ " = \"" ++ fromRepositoryConfig.table ++ "\"." ++ foreignKey, + .sql = try std.fmt.allocPrint(query.arena.allocator(), "INNER JOIN \"" ++ fromRepositoryConfig.table ++ "\" AS \"{s}related" ++ "\" ON " ++ + "\"" ++ toRepositoryConfig.table ++ "\"." ++ modelKey ++ " = \"{s}related" ++ "\"." ++ foreignKey, .{prefix, prefix}), .params = &[0]_sql.RawQueryParameter{}, })); @@ -260,20 +323,35 @@ fn typedOne( try query.whereIn(FromKeyType, "\"" ++ fromRepositoryConfig.table ++ "\".\"" ++ fromRepositoryConfig.key[0] ++ "\"", modelsIds); }, .reverse => { - // Build WHERE condition. - try query.whereIn(FromKeyType, "\"" ++ toRepositoryConfig.table ++ "\".\"" ++ foreignKey ++ "\"", modelsIds); - }, - .through => |through| { - query.join(.{ - .sql = "INNER JOIN \"" ++ through.table ++ "\" ON " ++ - "\"" ++ toRepositoryConfig.table ++ "\"." ++ modelKey ++ " = " ++ "\"" ++ through.table ++ "\"." ++ through.joinModelKey, + // Add SELECT. + query.select(.{ + .sql = baseSelect ++ ", \"" ++ toRepositoryConfig.table ++ "\".\"" ++ foreignKey ++ "\" AS \"__zrm_relation_key\"", .params = &[0]_sql.RawQueryParameter{}, }); // Build WHERE condition. - try query.whereIn(FromKeyType, "\"" ++ through.table ++ "\".\"" ++ through.joinForeignKey ++ "\"", modelsIds); + try query.whereIn(FromKeyType, "\"" ++ toRepositoryConfig.table ++ "\".\"" ++ foreignKey ++ "\"", modelsIds); + }, + .through => |through| { + // Add SELECT. + query.select(.{ + .sql = try std.fmt.allocPrint(query.arena.allocator(), baseSelect ++ ", \"{s}pivot" ++ "\".\"" ++ through.joinForeignKey ++ "\" AS \"__zrm_relation_key\"", .{prefix}), + .params = &[0]_sql.RawQueryParameter{}, + }); + + query.join(.{ + .sql = try std.fmt.allocPrint(query.arena.allocator(), "INNER JOIN \"" ++ through.table ++ "\" AS \"{s}pivot" ++ "\" ON " ++ + "\"" ++ toRepositoryConfig.table ++ "\"." ++ modelKey ++ " = " ++ "\"{s}pivot" ++ "\"." ++ through.joinModelKey, .{prefix, prefix}), + .params = &[0]_sql.RawQueryParameter{}, + }); + + // Build WHERE condition. + try query.whereIn(FromKeyType, try std.fmt.allocPrint(query.arena.allocator(), "\"{s}pivot" ++ "\".\"" ++ through.joinForeignKey ++ "\"", .{prefix}), modelsIds); }, } + + // Return built query. + return query; } pub fn relation(self: *Self) Relation(ToModel, ToTable) { @@ -285,6 +363,15 @@ fn typedOne( .inlineMapping = inlineMapping, .genJoin = genJoin, .genSelect = genSelect, + .getQueryType = getQueryType, + }, + }; + } + + pub fn runtimeRelation(self: *Self) RuntimeRelation { + return .{ + ._interface = .{ + .instance = self, .buildQuery = buildQuery, }, }; @@ -307,7 +394,7 @@ pub fn Relation(comptime ToModel: type, comptime ToTable: type) type { inlineMapping: *const fn (self: *anyopaque) bool, 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, + getQueryType: *const fn () type, }, /// Read the related model repository configuration. @@ -331,14 +418,30 @@ pub fn Relation(comptime ToModel: type, comptime ToTable: type) type { return self._interface.genSelect(self._interface.instance, table, prefix); } - /// Build the query to retrieve relation data. - /// Is always used when inline mapping is not possible, but also when loading relations lazily. - pub fn buildQuery(self: Self, models: []const anyopaque, query: *anyopaque) !void { - return self._interface.buildQuery(self._interface.instance, models, query); + /// Get relation query type. + pub fn getQueryType(self: Self) type { + return self._interface.getQueryType(); } }; } +/// Generic model runtime relation interface. +pub const RuntimeRelation = struct { + const Self = @This(); + + _interface: struct { + instance: *anyopaque, + + buildQuery: *const fn (self: *anyopaque, prefix: []const u8, models: []const *anyopaque, allocator: std.mem.Allocator, connector: _database.Connector) anyerror!*anyopaque, + }, + + /// Build the query to retrieve relation data. + /// Is always used when inline mapping is not possible, but also when loading relations lazily. + pub fn buildQuery(self: Self, prefix: []const u8, models: []const *anyopaque, allocator: std.mem.Allocator, connector: _database.Connector) !*anyopaque { + return self._interface.buildQuery(self._interface.instance, prefix, models, allocator, connector); + } +}; + /// A model relation object. pub const ModelRelation = struct { diff --git a/src/repository.zig b/src/repository.zig index 8f91410..79dc10d 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, null); + pub const Query: type = query.RepositoryQuery(Model, TableShape, config, null, null); pub const Insert: type = insert.RepositoryInsert(Model, TableShape, config, config.insertShape); /// Type of one model key. @@ -153,7 +153,7 @@ 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); + return query.RepositoryQuery(Model, TableShape, config, with, null); } pub fn InsertCustom(comptime InsertShape: type) type { diff --git a/src/result.zig b/src/result.zig index 9a690be..4c9cdad 100644 --- a/src/result.zig +++ b/src/result.zig @@ -1,20 +1,33 @@ const std = @import("std"); const zollections = @import("zollections"); +const _database = @import("database.zig"); const _repository = @import("repository.zig"); const _relations = @import("relations.zig"); +/// Structure of a model with its metadata. +pub fn ModelWithMetadata(comptime Model: type, comptime MetadataShape: ?type) type { + if (MetadataShape) |MetadataType| { + return struct { + model: Model, + metadata: MetadataType, + }; + } else { + return Model; + } +} + /// Type of a retrieved table data, with its retrieved relations. -pub fn TableWithRelations(comptime TableShape: type, comptime optionalRelations: ?[]const _relations.ModelRelation) type { +pub fn TableWithRelations(comptime TableShape: type, comptime MetadataShape: ?type, comptime optionalRelations: ?[]const _relations.ModelRelation) type { if (optionalRelations) |relations| { const tableType = @typeInfo(TableShape); // Build fields list: copy the existing table type fields and add those for relations. - var fields: [tableType.Struct.fields.len + relations.len]std.builtin.Type.StructField = undefined; + var fields: [tableType.Struct.fields.len + relations.len + (if (MetadataShape) |_| 1 else 0)]std.builtin.Type.StructField = undefined; // Copy base table fields. @memcpy(fields[0..tableType.Struct.fields.len], tableType.Struct.fields); // For each relation, create a new struct field in the table shape. - for (relations, fields[tableType.Struct.fields.len..]) |relation, *field| { + for (relations, fields[tableType.Struct.fields.len..(tableType.Struct.fields.len+relations.len)]) |relation, *field| { // Get relation field type (optional TableShape of the related value). comptime var relationImpl = relation.relation{}; const relationInstanceType = @TypeOf(relationImpl.relation()); @@ -34,6 +47,17 @@ pub fn TableWithRelations(comptime TableShape: type, comptime optionalRelations: }; } + if (MetadataShape) |MetadataType| { + // Add metadata field. + fields[tableType.Struct.fields.len + relations.len] = std.builtin.Type.StructField{ + .name = "_zrm_metadata", + .type = MetadataType, + .default_value = null, + .is_comptime = false, + .alignment = @alignOf(MetadataType), + }; + } + // Build the new type. return @Type(std.builtin.Type{ .Struct = .{ @@ -50,7 +74,7 @@ pub fn TableWithRelations(comptime TableShape: type, comptime optionalRelations: } /// Convert a value of the fully retrieved type to the TableShape type. -pub fn toTableShape(comptime TableShape: type, comptime optionalRelations: ?[]const _relations.ModelRelation, value: TableWithRelations(TableShape, optionalRelations)) TableShape { +pub fn toTableShape(comptime TableShape: type, comptime MetadataShape: ?type, comptime optionalRelations: ?[]const _relations.ModelRelation, value: TableWithRelations(TableShape, MetadataShape, optionalRelations)) TableShape { if (optionalRelations) |_| { // Make a structure of TableShape type. var tableValue: TableShape = undefined; @@ -69,7 +93,7 @@ pub fn toTableShape(comptime TableShape: type, comptime optionalRelations: ?[]co } /// Generic interface of a query result reader. -pub fn QueryResultReader(comptime TableShape: type, comptime inlineRelations: ?[]const _relations.ModelRelation) type { +pub fn QueryResultReader(comptime TableShape: type, comptime MetadataShape: ?type, comptime inlineRelations: ?[]const _relations.ModelRelation) type { return struct { const Self = @This(); @@ -77,12 +101,12 @@ pub fn QueryResultReader(comptime TableShape: type, comptime inlineRelations: ?[ pub const Instance = struct { __interface: struct { instance: *anyopaque, - next: *const fn (self: *anyopaque) anyerror!?TableWithRelations(TableShape, inlineRelations), + next: *const fn (self: *anyopaque) anyerror!?TableWithRelations(TableShape, MetadataShape, inlineRelations), }, allocator: std.mem.Allocator, - pub fn next(self: Instance) !?TableWithRelations(TableShape, inlineRelations) { + pub fn next(self: Instance) !?TableWithRelations(TableShape, MetadataShape, inlineRelations) { return self.__interface.next(self.__interface.instance); } }; @@ -100,11 +124,13 @@ pub fn QueryResultReader(comptime TableShape: type, comptime inlineRelations: ?[ } /// Map query result to repository model structures, and load the given relations. -pub fn ResultMapper(comptime Model: type, comptime TableShape: type, comptime repositoryConfig: _repository.RepositoryConfiguration(Model, TableShape), comptime inlineRelations: ?[]const _relations.ModelRelation, comptime relations: ?[]const _relations.ModelRelation) type { - _ = relations; +pub fn ResultMapper(comptime Model: type, comptime TableShape: type, comptime MetadataShape: ?type, comptime repositoryConfig: _repository.RepositoryConfiguration(Model, TableShape), comptime inlineRelations: ?[]const _relations.ModelRelation, comptime relations: ?[]const _relations.ModelRelation) type { return struct { /// Map the query result to a repository result, with all the required relations. - pub fn map(allocator: std.mem.Allocator, queryReader: QueryResultReader(TableShape, inlineRelations)) !_repository.RepositoryResult(Model) { + pub fn map(comptime withMetadata: bool, allocator: std.mem.Allocator, connector: _database.Connector, queryReader: QueryResultReader(TableShape, MetadataShape, inlineRelations)) !_repository.RepositoryResult(if (withMetadata) ModelWithMetadata(Model, MetadataShape) else Model) { + // Get result type depending on metadata + const ResultType = if (withMetadata) ModelWithMetadata(Model, MetadataShape) else Model; + // Create an arena for mapper data. var mapperArena = std.heap.ArenaAllocator.init(allocator); @@ -112,14 +138,14 @@ pub fn ResultMapper(comptime Model: type, comptime TableShape: type, comptime re const reader = try queryReader.init(mapperArena.allocator()); // Initialize models list. - var models = std.ArrayList(*Model).init(allocator); + var models = std.ArrayList(*ResultType).init(allocator); defer models.deinit(); // Get all raw models from the result reader. while (try reader.next()) |rawModel| { // Parse each raw model from the reader. - const model = try allocator.create(Model); - model.* = try repositoryConfig.fromSql(toTableShape(TableShape, inlineRelations, rawModel)); + const model = try allocator.create(ResultType); + (if (withMetadata) model.model else model.*) = try repositoryConfig.fromSql(toTableShape(TableShape, MetadataShape, inlineRelations, rawModel)); // Map inline relations. if (inlineRelations) |_inlineRelations| { @@ -136,14 +162,69 @@ pub fn ResultMapper(comptime Model: type, comptime TableShape: type, comptime re } } + if (withMetadata) { + // Set model metadata. + model.metadata = rawModel._zrm_metadata; + } + try models.append(model); } - //TODO load relations? + if (relations) |relationsToLoad| { + inline for (relationsToLoad) |relation| { + const comptimeRelation = @constCast(&relation.relation{}).relation(); + var relationImpl = relation.relation{}; + const relationInstance = relationImpl.runtimeRelation(); + + // Build query for the relation to get. + const query: *comptimeRelation.getQueryType() = @ptrCast(@alignCast( + try relationInstance.buildQuery("relations." ++ relation.field ++ ".", @ptrCast(models.items), allocator, connector) + )); + defer { + query.deinit(); + allocator.destroy(query); + } + + // Get related models. + const relatedModels = try query.getWithMetadata(mapperArena.allocator()); + + // Create a map with related models. + const RelatedModelsListType = std.ArrayList(@TypeOf(relatedModels.models[0].model)); + const RelatedModelsMapType = std.AutoHashMap(std.meta.FieldType(@TypeOf(relatedModels.models[0].metadata), .__zrm_relation_key), RelatedModelsListType); + var relatedModelsMap = RelatedModelsMapType.init(allocator); + defer relatedModelsMap.deinit(); + + // Fill the map of related models, indexing them by the relation key. + for (relatedModels.models) |relatedModel| { + // For each related model, put it in the map at the relation key. + var modelsList = try relatedModelsMap.getOrPut(relatedModel.metadata.__zrm_relation_key); + + if (!modelsList.found_existing) { + // Initialize the related models list. + modelsList.value_ptr.* = RelatedModelsListType.init(mapperArena.allocator()); + } + + // Add the current related model to the list. + try modelsList.value_ptr.append(relatedModel.model); + } + + // For each model, at the grouped related models if there are some. + for (models.items) |model| { + @field(model, relation.field) = ( + if (relatedModelsMap.getPtr(@field(model, repositoryConfig.key[0]))) |relatedModelsList| + // There are related models, set them. + try relatedModelsList.toOwnedSlice() + else + // No related models, set an empty array. + &[0](@TypeOf(relatedModels.models[0].model)){} + ); + } + } + } // Return a result with the models. - return _repository.RepositoryResult(Model).init(allocator, - zollections.Collection(Model).init(allocator, try models.toOwnedSlice()), + return _repository.RepositoryResult(ResultType).init(allocator, + zollections.Collection(ResultType).init(allocator, try models.toOwnedSlice()), mapperArena, ); } diff --git a/src/sql.zig b/src/sql.zig index 94cc2ac..11b9e10 100644 --- a/src/sql.zig +++ b/src/sql.zig @@ -216,11 +216,11 @@ 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 = ""; + comptime var columnsSelect: []const u8 = ""; - var first = true; + comptime var first = true; // For each field, generate a format string. - for (@typeInfo(TableShape).Struct.fields) |field| { + inline for (@typeInfo(TableShape).Struct.fields) |field| { // Add ", " between all selected columns (just not the first one). if (first) { first = false; diff --git a/src/update.zig b/src/update.zig index 153d366..5add4d3 100644 --- a/src/update.zig +++ b/src/update.zig @@ -59,7 +59,7 @@ pub fn RepositoryUpdate(comptime Model: type, comptime TableShape: type, comptim const Configuration = RepositoryUpdateConfiguration(UpdateShape); /// Result mapper type. - pub const ResultMapper = _result.ResultMapper(Model, TableShape, repositoryConfig, null, null); + pub const ResultMapper = _result.ResultMapper(Model, TableShape, null, repositoryConfig, null, null); arena: std.heap.ArenaAllocator, connector: database.Connector, @@ -305,8 +305,8 @@ pub fn RepositoryUpdate(comptime Model: type, comptime TableShape: type, comptim defer queryResult.deinit(); // Map query results. - var postgresqlReader = postgresql.QueryResultReader(TableShape, null).init(queryResult); - return try ResultMapper.map(allocator, postgresqlReader.reader()); + var postgresqlReader = postgresql.QueryResultReader(TableShape, null, null).init(queryResult); + return try ResultMapper.map(false, allocator, self.connector, postgresqlReader.reader()); } /// Initialize a new repository update query. diff --git a/tests/relations.zig b/tests/relations.zig index 375908e..1be80f8 100644 --- a/tests/relations.zig +++ b/tests/relations.zig @@ -18,7 +18,7 @@ fn initDatabase(allocator: std.mem.Allocator) !void { .password = "zrm", .database = "zrm", }, - .size = 1, + .size = 5, }); } @@ -49,7 +49,53 @@ test "belongsTo" { try std.testing.expectEqual(1, result.models[0].parent_id); try std.testing.expectEqual(1, result.models[1].parent_id); try std.testing.expectEqual(repository.MyModel, @TypeOf(result.models[0].parent.?)); - try std.testing.expectEqual(repository.MyModel, @TypeOf(result.models[1].parent.?)); try std.testing.expectEqual(1, result.models[0].parent.?.id); try std.testing.expectEqual(1, result.models[1].parent.?.id); } + +test "hasMany" { + zrm.setDebug(true); + + try initDatabase(std.testing.allocator); + defer database.deinit(); + var poolConnector = zrm.database.PoolConnector{ + .pool = database, + }; + + // Build a query of submodels. + var myQuery = repository.MyModelRepository.QueryWith( + // Retrieve parents of submodels from relation. + &[_]zrm.relations.ModelRelation{repository.MyModelRelations.submodels} + ).init(std.testing.allocator, poolConnector.connector(), .{}); + defer myQuery.deinit(); + + try myQuery.buildSql(); + + // Get query result. + var result = try myQuery.get(std.testing.allocator); + defer result.deinit(); + + // Checking result. + try std.testing.expectEqual(4, result.models.len); + try std.testing.expectEqual(repository.MySubmodel, @TypeOf(result.models[0].submodels.?[0])); + + // Checking retrieved submodels. + for (result.models) |model| { + try std.testing.expect(model.submodels != null); + + if (model.submodels.?.len > 0) { + try std.testing.expectEqual(1, model.id); + try std.testing.expectEqual(2, model.submodels.?.len); + for (model.submodels.?) |submodel| { + try std.testing.expectEqual(1, submodel.parent_id.?); + try std.testing.expect( + std.mem.eql(u8, &try pg.uuidToHex(submodel.uuid), "f6868a5b-2efc-455f-b76e-872df514404f") + or std.mem.eql(u8, &try pg.uuidToHex(submodel.uuid), "013ef171-9781-40e9-b843-f6bc11890070") + ); + try std.testing.expect( + std.mem.eql(u8, submodel.label, "test") or std.mem.eql(u8, submodel.label, "another") + ); + } + } + } +} diff --git a/tests/repository.zig b/tests/repository.zig index 4735d95..6faeb6f 100644 --- a/tests/repository.zig +++ b/tests/repository.zig @@ -22,7 +22,7 @@ fn initDatabase() !void { } /// An example submodel, child of the example model. -const MySubmodel = struct { +pub const MySubmodel = struct { uuid: []const u8, label: []const u8,