diff --git a/src/postgresql.zig b/src/postgresql.zig index 2a5623d..cb72c80 100644 --- a/src/postgresql.zig +++ b/src/postgresql.zig @@ -162,7 +162,7 @@ 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 MetadataShape: ?type, comptime inlineRelations: ?[]const _relations.ModelRelation) type { +pub fn QueryResultReader(comptime TableShape: type, comptime MetadataShape: ?type, comptime inlineRelations: ?[]const _relations.Relation) type { const InstanceInterface = _result.QueryResultReader(TableShape, MetadataShape, inlineRelations).Instance; // Build relations mappers container type. @@ -173,9 +173,7 @@ pub fn QueryResultReader(comptime TableShape: type, comptime MetadataShape: ?typ for (_inlineRelations, &fields) |relation, *field| { // Get relation field type (TableShape of the related value). - var relationImpl = relation.relation{}; - const relationInstanceType = @TypeOf(relationImpl.relation()); - const relationFieldType = PgMapper(relationInstanceType.TableShape); + const relationFieldType = PgMapper(relation.TableShape); field.* = .{ .name = relation.field ++ [0:0]u8{}, @@ -278,11 +276,8 @@ pub fn QueryResultReader(comptime TableShape: type, comptime MetadataShape: ?typ if (inlineRelations) |_inlineRelations| { // Initialize mapper for each relation. inline for (_inlineRelations) |relation| { - // Get relation field type (TableShape of the related value). - comptime var relationImpl = relation.relation{}; - const relationInstanceType = @TypeOf(relationImpl.relation()); @field(self.instance.relationsMappers, relation.field) = - try makeMapper(relationInstanceType.TableShape, self.result, allocator, "relations." ++ relation.field ++ "."); + try makeMapper(relation.TableShape, self.result, allocator, "relations." ++ relation.field ++ "."); } } diff --git a/src/query.zig b/src/query.zig index e092d4e..929ca09 100644 --- a/src/query.zig +++ b/src/query.zig @@ -20,20 +20,20 @@ pub const RepositoryQueryConfiguration = struct { /// Compiled relations structure. const CompiledRelations = struct { - inlineRelations: []relations.ModelRelation, - otherRelations: []relations.ModelRelation, + inlineRelations: []relations.Relation, + otherRelations: []relations.Relation, 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), comptime with: ?[]const relations.ModelRelation, comptime MetadataShape: ?type) type { +pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime repositoryConfig: repository.RepositoryConfiguration(Model, TableShape), comptime with: ?[]const relations.Relation, comptime MetadataShape: ?type) type { const compiledRelations = comptime compile: { // Inline relations list. - var inlineRelations: []relations.ModelRelation = &[0]relations.ModelRelation{}; + var inlineRelations: []relations.Relation = &[0]relations.Relation{}; // Other relations list. - var otherRelations: []relations.ModelRelation = &[0]relations.ModelRelation{}; + var otherRelations: []relations.Relation = &[0]relations.Relation{}; if (with) |_with| { // If there are relations to eager load, prepare their query. @@ -45,20 +45,14 @@ pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime for (_with) |relation| { // For each relation, determine if it's inline or not. - var relationImpl = relation.relation{}; - const relationInstance = relationImpl.relation(); - if (relationInstance.inlineMapping()) { + if (relation.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)))); + inlineSelect = @ptrCast(@constCast(_comptime.append(inlineSelect, relation.select))); // Generate joined table for the relation. - inlineJoins = @ptrCast(@constCast(_comptime.append(inlineJoins, relationInstance.genJoin(tableAlias)))); + inlineJoins = @ptrCast(@constCast(_comptime.append(inlineJoins, relation.join))); } else { // Add the current relation to other relations. otherRelations = @ptrCast(@constCast(_comptime.append(otherRelations, relation))); @@ -73,8 +67,8 @@ pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime }; } else { break :compile CompiledRelations{ - .inlineRelations = &[0]relations.ModelRelation{}, - .otherRelations = &[0]relations.ModelRelation{}, + .inlineRelations = &[0]relations.Relation{}, + .otherRelations = &[0]relations.Relation{}, .inlineSelect = "", .inlineJoins = "", }; diff --git a/src/relations.zig b/src/relations.zig index 64b2077..2f4707b 100644 --- a/src/relations.zig +++ b/src/relations.zig @@ -50,116 +50,106 @@ pub fn typedMany( comptime toRepositoryConfig: repository.RepositoryConfiguration(ToModel, ToTable), comptime config: ManyConfiguration) type { - // Get foreign key from relation config or repository config. - const foreignKey = switch (config) { - .direct => |direct| direct.foreignKey, - .through => |through| if (through.foreignKey) |_foreignKey| _foreignKey else toRepositoryConfig.key[0], - }; - - // Get model key from relation config or repository config. - const modelKey = switch (config) { - .direct => |direct| if (direct.modelKey) |_modelKey| _modelKey else fromRepositoryConfig.key[0], - .through => |through| if (through.modelKey) |_modelKey| _modelKey else fromRepositoryConfig.key[0], - }; - - const FromKeyType = std.meta.fields(FromModel)[std.meta.fieldIndex(FromModel, fromRepositoryConfig.key[0]).?].type; - const QueryType = _query.RepositoryQuery(ToModel, ToTable, toRepositoryConfig, null, struct { - __zrm_relation_key: FromKeyType, - }); - return struct { - const Self = @This(); - - fn getRepositoryConfiguration(_: *anyopaque) repository.RepositoryConfiguration(ToModel, ToTable) { - return toRepositoryConfig; - } - - fn inlineMapping(_: *anyopaque) bool { - return false; - } - - fn genJoin(_: *anyopaque, comptime _: []const u8) []const u8 { - unreachable; // No possible join in a many relation. - } - - fn _genSelect(comptime table: []const u8, comptime prefix: []const u8) []const u8 { - return _sql.SelectBuild(ToTable, table, prefix); - } - - fn genSelect(_: *anyopaque, comptime table: []const u8, comptime prefix: []const u8) []const u8 { - return _genSelect(table, prefix); - } - - 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); - for (models, modelsIds) |model, *modelId| { - modelId.* = @field(model, fromRepositoryConfig.key[0]); - } - - switch (config) { - .direct => { - // Add SELECT. - query.select(.{ - .sql = baseSelect ++ ", \"" ++ toRepositoryConfig.table ++ "\".\"" ++ foreignKey ++ "\" AS \"__zrm_relation_key\"", - .params = &[0]_sql.RawQueryParameter{}, - }); - - // Build WHERE condition. - 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) { - return .{ - ._interface = .{ - .instance = self, - - .getRepositoryConfiguration = getRepositoryConfiguration, - .inlineMapping = inlineMapping, - .genJoin = genJoin, - .genSelect = genSelect, - }, - .QueryType = QueryType, + /// Relation implementation. + pub fn Implementation(field: []const u8) type { + // Get foreign key from relation config or repository config. + const foreignKey = switch (config) { + .direct => |direct| direct.foreignKey, + .through => |through| if (through.foreignKey) |_foreignKey| _foreignKey else toRepositoryConfig.key[0], }; - } - pub fn runtimeRelation(self: *Self) RuntimeRelation { - return .{ - ._interface = .{ - .instance = self, - .buildQuery = buildQuery, - }, + // Get model key from relation config or repository config. + const modelKey = switch (config) { + .direct => |direct| if (direct.modelKey) |_modelKey| _modelKey else fromRepositoryConfig.key[0], + .through => |through| if (through.modelKey) |_modelKey| _modelKey else fromRepositoryConfig.key[0], + }; + + const FromKeyType = std.meta.fields(FromModel)[std.meta.fieldIndex(FromModel, fromRepositoryConfig.key[0]).?].type; + const QueryType = _query.RepositoryQuery(ToModel, ToTable, toRepositoryConfig, null, struct { + __zrm_relation_key: FromKeyType, + }); + + const alias = "relations." ++ field; + const prefix = alias ++ "."; + + return struct { + const Self = @This(); + + fn genSelect() []const u8 { + return _sql.SelectBuild(ToTable, alias, prefix); + } + + fn buildQuery(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 _sql.SelectBuild(ToTable, toRepositoryConfig.table, ""); + + // Prepare given models IDs. + const modelsIds = try query.arena.allocator().alloc(FromKeyType, models.len); + for (models, modelsIds) |model, *modelId| { + modelId.* = @field(model, fromRepositoryConfig.key[0]); + } + + switch (config) { + .direct => { + // Add SELECT. + query.select(.{ + .sql = baseSelect ++ ", \"" ++ toRepositoryConfig.table ++ "\".\"" ++ foreignKey ++ "\" AS \"__zrm_relation_key\"", + .params = &[0]_sql.RawQueryParameter{}, + }); + + // Build WHERE condition. + 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. + } + + /// Build the "many" generic relation. + pub fn relation(_: Self) Relation { + return .{ + ._interface = .{ + .repositoryConfiguration = &toRepositoryConfig, + + .buildQuery = buildQuery, + }, + .Model = ToModel, + .TableShape = ToTable, + .field = field, + .alias = alias, + .prefix = prefix, + .QueryType = QueryType, + + .inlineMapping = false, + .join = undefined, + .select = genSelect(), + }; + } }; } }; @@ -220,228 +210,198 @@ fn typedOne( comptime toRepositoryConfig: repository.RepositoryConfiguration(ToModel, ToTable), 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, struct { - __zrm_relation_key: FromKeyType, - }); - - // Get foreign key from relation config or repository config. - const foreignKey = switch (config) { - .direct => |direct| direct.foreignKey, - .reverse => |reverse| reverse.foreignKey, - .through => |through| if (through.foreignKey) |_foreignKey| _foreignKey else fromRepositoryConfig.key[0], - }; - - // Get model key from relation config or repository config. - const modelKey = switch (config) { - .direct => |direct| if (direct.modelKey) |_modelKey| _modelKey else toRepositoryConfig.key[0], - .reverse => |reverse| if (reverse.modelKey) |_modelKey| _modelKey else toRepositoryConfig.key[0], - .through => |through| if (through.modelKey) |_modelKey| _modelKey else toRepositoryConfig.key[0], - }; - return struct { - const Self = @This(); + pub fn Implementation(field: []const u8) type { + const FromKeyType = std.meta.fields(FromModel)[std.meta.fieldIndex(FromModel, fromRepositoryConfig.key[0]).?].type; + const QueryType = _query.RepositoryQuery(ToModel, ToTable, toRepositoryConfig, null, struct { + __zrm_relation_key: FromKeyType, + }); - fn getRepositoryConfiguration(_: *anyopaque) repository.RepositoryConfiguration(ToModel, ToTable) { - return toRepositoryConfig; - } - - fn inlineMapping(_: *anyopaque) bool { - return true; - } - - fn genJoin(_: *anyopaque, comptime alias: []const u8) []const u8 { - return switch (config) { - .direct => ( - "LEFT JOIN \"" ++ toRepositoryConfig.table ++ "\" AS \"" ++ alias ++ "\" ON " ++ - "\"" ++ fromRepositoryConfig.table ++ "\"." ++ foreignKey ++ " = \"" ++ alias ++ "\"." ++ modelKey - ), - - .reverse => ( - "LEFT JOIN \"" ++ toRepositoryConfig.table ++ "\" AS \"" ++ alias ++ "\" ON " ++ - "\"" ++ fromRepositoryConfig.table ++ "\"." ++ modelKey ++ " = \"" ++ alias ++ "\"." ++ foreignKey - ), - - .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 - ), + // Get foreign key from relation config or repository config. + const foreignKey = switch (config) { + .direct => |direct| direct.foreignKey, + .reverse => |reverse| reverse.foreignKey, + .through => |through| if (through.foreignKey) |_foreignKey| _foreignKey else fromRepositoryConfig.key[0], }; - } - fn _genSelect(comptime table: []const u8, comptime prefix: []const u8) []const u8 { - return _sql.SelectBuild(ToTable, table, prefix); - } - - fn genSelect(_: *anyopaque, comptime table: []const u8, comptime prefix: []const u8) []const u8 { - return _genSelect(table, prefix); - } - - 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); - for (models, modelsIds) |model, *modelId| { - modelId.* = @field(model, fromRepositoryConfig.key[0]); - } - - 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 = 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{}, - })); - - // Build WHERE condition. - try query.whereIn(FromKeyType, "\"" ++ fromRepositoryConfig.table ++ "\".\"" ++ fromRepositoryConfig.key[0] ++ "\"", modelsIds); - }, - .reverse => { - // Add SELECT. - query.select(.{ - .sql = baseSelect ++ ", \"" ++ toRepositoryConfig.table ++ "\".\"" ++ foreignKey ++ "\" AS \"__zrm_relation_key\"", - .params = &[0]_sql.RawQueryParameter{}, - }); - - // Build WHERE condition. - 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) { - return .{ - ._interface = .{ - .instance = self, - - .getRepositoryConfiguration = getRepositoryConfiguration, - .inlineMapping = inlineMapping, - .genJoin = genJoin, - .genSelect = genSelect, - }, - .QueryType = QueryType, + // Get model key from relation config or repository config. + const modelKey = switch (config) { + .direct => |direct| if (direct.modelKey) |_modelKey| _modelKey else toRepositoryConfig.key[0], + .reverse => |reverse| if (reverse.modelKey) |_modelKey| _modelKey else toRepositoryConfig.key[0], + .through => |through| if (through.modelKey) |_modelKey| _modelKey else toRepositoryConfig.key[0], }; - } - pub fn runtimeRelation(self: *Self) RuntimeRelation { - return .{ - ._interface = .{ - .instance = self, - .buildQuery = buildQuery, - }, + const alias = "relations." ++ field; + const prefix = alias ++ "."; + + return struct { + const Self = @This(); + + fn genJoin() []const u8 { + return switch (config) { + .direct => ( + "LEFT JOIN \"" ++ toRepositoryConfig.table ++ "\" AS \"" ++ alias ++ "\" ON " ++ + "\"" ++ fromRepositoryConfig.table ++ "\"." ++ foreignKey ++ " = \"" ++ alias ++ "\"." ++ modelKey + ), + + .reverse => ( + "LEFT JOIN \"" ++ toRepositoryConfig.table ++ "\" AS \"" ++ alias ++ "\" ON " ++ + "\"" ++ fromRepositoryConfig.table ++ "\"." ++ modelKey ++ " = \"" ++ alias ++ "\"." ++ foreignKey + ), + + .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() []const u8 { + return _sql.SelectBuild(ToTable, alias, prefix); + } + + fn buildQuery(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 _sql.SelectBuild(ToTable, toRepositoryConfig.table, ""); + + // Prepare given models IDs. + const modelsIds = try query.arena.allocator().alloc(FromKeyType, models.len); + for (models, modelsIds) |model, *modelId| { + modelId.* = @field(model, fromRepositoryConfig.key[0]); + } + + 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 = 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{}, + })); + + // Build WHERE condition. + try query.whereIn(FromKeyType, "\"" ++ fromRepositoryConfig.table ++ "\".\"" ++ fromRepositoryConfig.key[0] ++ "\"", modelsIds); + }, + .reverse => { + // Add SELECT. + query.select(.{ + .sql = baseSelect ++ ", \"" ++ toRepositoryConfig.table ++ "\".\"" ++ foreignKey ++ "\" AS \"__zrm_relation_key\"", + .params = &[0]_sql.RawQueryParameter{}, + }); + + // Build WHERE condition. + 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; + } + + /// Build the "one" generic relation. + pub fn relation(_: Self) Relation { + return .{ + ._interface = .{ + .repositoryConfiguration = &toRepositoryConfig, + + .buildQuery = buildQuery, + }, + .Model = ToModel, + .TableShape = ToTable, + .field = field, + .alias = alias, + .prefix = prefix, + .QueryType = QueryType, + + .inlineMapping = true, + .join = genJoin(), + .select = genSelect(), + }; + } }; } }; } /// Generic model relation interface. -pub fn Relation(comptime ToModel: type, comptime ToTable: type) type { - return struct { - const Self = @This(); - - pub const Model = ToModel; - pub const TableShape = ToTable; - - _interface: struct { - instance: *anyopaque, - - getRepositoryConfiguration: *const fn (self: *anyopaque) repository.RepositoryConfiguration(ToModel, ToTable), - 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, - }, - - QueryType: type, - - /// Read the related model repository configuration. - pub fn getRepositoryConfiguration(self: Self) repository.RepositoryConfiguration(ToModel, ToTable) { - return self._interface.getRepositoryConfiguration(self._interface.instance); - } - - /// Relation mapping is done inline: this means that it's done at the same time the model is mapped, - /// and that the associated data will be retrieved in the main query. - pub fn inlineMapping(self: Self) bool { - return self._interface.inlineMapping(self._interface.instance); - } - - /// In case of inline mapping, generate a JOIN clause to retrieve the associated data. - 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, comptime table: []const u8, comptime prefix: []const u8) []const u8 { - return self._interface.genSelect(self._interface.instance, table, prefix); - } - }; -} - -/// Generic model runtime relation interface. -pub const RuntimeRelation = struct { +pub const Relation = struct { const Self = @This(); _interface: struct { - instance: *anyopaque, + repositoryConfiguration: *const anyopaque, - buildQuery: *const fn (self: *anyopaque, prefix: []const u8, models: []const *anyopaque, allocator: std.mem.Allocator, connector: _database.Connector) anyerror!*anyopaque, + buildQuery: *const fn (models: []const *anyopaque, allocator: std.mem.Allocator, connector: _database.Connector) anyerror!*anyopaque, }, + /// Type of the related model. + Model: type, + /// Type of the related model table. + TableShape: type, + /// Field where to put the related model(s). + field: []const u8, + /// Table alias of the relation. + alias: []const u8, + /// Prefix of fields of the relation. + prefix: []const u8, + /// Type of a query of the related models. + QueryType: type, + + /// Set if relation mapping is done inline: this means that it's done at the same time the model is mapped, + /// and that the associated data will be retrieved in the main query. + inlineMapping: bool, + /// In case of inline mapping, the JOIN clause to retrieve the associated data. + join: []const u8, + /// The SELECT clause to retrieve the associated data. + select: []const u8, + /// 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); + pub fn buildQuery(self: Self, models: []const *anyopaque, allocator: std.mem.Allocator, connector: _database.Connector) !*anyopaque { + return self._interface.buildQuery(models, allocator, connector); } -}; - -/// A model relation object. -pub const ModelRelation = struct { - relation: type, - field: []const u8, + /// Get typed repository configuration for the related model. + pub fn repositoryConfiguration(self: Self) repository.RepositoryConfiguration(self.Model, self.TableShape) { + const repoConfig: *const repository.RepositoryConfiguration(self.Model, self.TableShape) + = @ptrCast(@alignCast(self._interface.repositoryConfiguration)); + return repoConfig.*; + } }; /// Structure of an eager loaded relation. pub const Eager = struct { - /// Model field to fill for the relation. - field: []const u8, /// The relation to eager load. relation: Relation, /// Subrelations to eager load. diff --git a/src/repository.zig b/src/repository.zig index 79dc10d..4ac965d 100644 --- a/src/repository.zig +++ b/src/repository.zig @@ -78,18 +78,46 @@ pub fn ModelKeyType(comptime Model: type, comptime TableShape: type, comptime co pub fn RelationsDefinitionType(comptime rawDefinition: anytype) type { const rawDefinitionType = @typeInfo(@TypeOf(rawDefinition)); - // Build model relations fields. - var fields: [rawDefinitionType.Struct.fields.len]std.builtin.Type.StructField = undefined; - inline for (rawDefinitionType.Struct.fields, &fields) |originalField, *field| { - field.* = .{ + // Build relations fields and implementations fields. + var fields: [1 + rawDefinitionType.Struct.fields.len]std.builtin.Type.StructField = undefined; + var implementationsFields: [rawDefinitionType.Struct.fields.len]std.builtin.Type.StructField = undefined; + + inline for (rawDefinitionType.Struct.fields, fields[1..], &implementationsFields) |originalField, *field, *implementationField| { + field.* = std.builtin.Type.StructField{ .name = originalField.name, - .type = _relations.ModelRelation, + .type = _relations.Relation, .default_value = null, .is_comptime = false, - .alignment = @alignOf(_relations.ModelRelation), + .alignment = @alignOf(type), + }; + + const ImplementationType = @field(rawDefinition, originalField.name).Implementation(originalField.name); + implementationField.* = std.builtin.Type.StructField{ + .name = originalField.name, + .type = ImplementationType, + .default_value = &ImplementationType{}, + .is_comptime = false, + .alignment = @alignOf(ImplementationType), }; } + // Add implementations field. + const ImplementationsType = @Type(.{ + .Struct = std.builtin.Type.Struct{ + .layout = std.builtin.Type.ContainerLayout.auto, + .fields = &implementationsFields, + .decls = &[_]std.builtin.Type.Declaration{}, + .is_tuple = false, + }, + }); + fields[0] = std.builtin.Type.StructField{ + .name = "_implementations", + .type = ImplementationsType, + .default_value = null, + .is_comptime = false, + .alignment = @alignOf(ImplementationsType), + }; + // Return built type. return @Type(.{ .Struct = std.builtin.Type.Struct{ @@ -134,17 +162,16 @@ pub fn Repository(comptime Model: type, comptime TableShape: type, comptime repo // Initialize final relations definition. var definition: RelationsDefinitionType(rawDefinition) = undefined; + definition._implementations = .{}; + // Check that the definition structure only include known fields. inline for (std.meta.fieldNames(rawDefinitionType)) |fieldName| { if (!@hasField(Model, fieldName)) { @compileError("No corresponding field for relation " ++ fieldName); } - // Alter definition structure to add the field name. - @field(definition, fieldName) = .{ - .relation = @field(rawDefinition, fieldName), - .field = fieldName, - }; + // Alter definition structure to set the relation instance. + @field(definition, fieldName) = @field(definition._implementations, fieldName).relation(); } // Return altered definition structure. @@ -152,7 +179,7 @@ pub fn Repository(comptime Model: type, comptime TableShape: type, comptime repo } }; - pub fn QueryWith(comptime with: []const _relations.ModelRelation) type { + pub fn QueryWith(comptime with: []const _relations.Relation) type { return query.RepositoryQuery(Model, TableShape, config, with, null); } diff --git a/src/result.zig b/src/result.zig index 36aa4f4..f9b4d33 100644 --- a/src/result.zig +++ b/src/result.zig @@ -17,7 +17,7 @@ pub fn ModelWithMetadata(comptime Model: type, comptime MetadataShape: ?type) ty } /// Type of a retrieved table data, with its retrieved relations. -pub fn TableWithRelations(comptime TableShape: type, comptime MetadataShape: ?type, comptime optionalRelations: ?[]const _relations.ModelRelation) type { +pub fn TableWithRelations(comptime TableShape: type, comptime MetadataShape: ?type, comptime optionalRelations: ?[]const _relations.Relation) type { if (optionalRelations) |relations| { const tableType = @typeInfo(TableShape); @@ -29,11 +29,9 @@ pub fn TableWithRelations(comptime TableShape: type, comptime MetadataShape: ?ty // For each relation, create a new struct field in the table shape. 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()); const relationFieldType = @Type(std.builtin.Type{ .Optional = .{ - .child = relationInstanceType.TableShape + .child = relation.TableShape }, }); @@ -74,7 +72,7 @@ pub fn TableWithRelations(comptime TableShape: type, comptime MetadataShape: ?ty } /// Convert a value of the fully retrieved type to the TableShape type. -pub fn toTableShape(comptime TableShape: type, comptime MetadataShape: ?type, comptime optionalRelations: ?[]const _relations.ModelRelation, value: TableWithRelations(TableShape, MetadataShape, optionalRelations)) TableShape { +pub fn toTableShape(comptime TableShape: type, comptime MetadataShape: ?type, comptime optionalRelations: ?[]const _relations.Relation, value: TableWithRelations(TableShape, MetadataShape, optionalRelations)) TableShape { if (optionalRelations) |_| { // Make a structure of TableShape type. var tableValue: TableShape = undefined; @@ -93,7 +91,7 @@ pub fn toTableShape(comptime TableShape: type, comptime MetadataShape: ?type, co } /// Generic interface of a query result reader. -pub fn QueryResultReader(comptime TableShape: type, comptime MetadataShape: ?type, comptime inlineRelations: ?[]const _relations.ModelRelation) type { +pub fn QueryResultReader(comptime TableShape: type, comptime MetadataShape: ?type, comptime inlineRelations: ?[]const _relations.Relation) type { return struct { const Self = @This(); @@ -124,7 +122,7 @@ pub fn QueryResultReader(comptime TableShape: type, comptime MetadataShape: ?typ } /// Map query result to repository model structures, and load the given 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 { +pub fn ResultMapper(comptime Model: type, comptime TableShape: type, comptime MetadataShape: ?type, comptime repositoryConfig: _repository.RepositoryConfiguration(Model, TableShape), comptime inlineRelations: ?[]const _relations.Relation, comptime relations: ?[]const _relations.Relation) type { return struct { /// Map the query result to a repository result, with all the required relations. 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) { @@ -151,12 +149,10 @@ pub fn ResultMapper(comptime Model: type, comptime TableShape: type, comptime Me if (inlineRelations) |_inlineRelations| { // If there are loaded inline relations, map them to the result. inline for (_inlineRelations) |relation| { - comptime var relationImpl = relation.relation{}; - const relationInstance = relationImpl.relation(); // Set the read inline relation value. @field(model.*, relation.field) = ( if (@field(rawModel, relation.field)) |relationVal| - try relationInstance.getRepositoryConfiguration().fromSql(relationVal) + try relation.repositoryConfiguration().fromSql(relationVal) else null ); } @@ -172,13 +168,9 @@ pub fn ResultMapper(comptime Model: type, comptime TableShape: type, comptime Me 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.QueryType = @ptrCast(@alignCast( - try relationInstance.buildQuery("relations." ++ relation.field ++ ".", @ptrCast(models.items), allocator, connector) + const query: *relation.QueryType = @ptrCast(@alignCast( + try relation.buildQuery(@ptrCast(models.items), allocator, connector) )); defer { query.deinit(); diff --git a/tests/relations.zig b/tests/relations.zig index 1be80f8..ef9d943 100644 --- a/tests/relations.zig +++ b/tests/relations.zig @@ -34,7 +34,7 @@ test "belongsTo" { // Build a query of submodels. var myQuery = repository.MySubmodelRepository.QueryWith( // Retrieve parents of submodels from relation. - &[_]zrm.relations.ModelRelation{repository.MySubmodelRelations.parent} + &[_]zrm.relations.Relation{repository.MySubmodelRelations.parent} ).init(std.testing.allocator, poolConnector.connector(), .{}); defer myQuery.deinit(); @@ -64,8 +64,8 @@ test "hasMany" { // Build a query of submodels. var myQuery = repository.MyModelRepository.QueryWith( - // Retrieve parents of submodels from relation. - &[_]zrm.relations.ModelRelation{repository.MyModelRelations.submodels} + // Retrieve parents of submodels from relation. + &[_]zrm.relations.Relation{repository.MyModelRelations.submodels} ).init(std.testing.allocator, poolConnector.connector(), .{}); defer myQuery.deinit();