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"); /// Configure a "one to many" or "many to many" relationship. pub const ManyConfiguration = union(enum) { /// Direct one-to-many relationship using a distant foreign key. direct: struct { /// The distant foreign key name pointing to the current model. foreignKey: []const u8, /// Current model key name. /// Use the default key name of the current model. modelKey: ?[]const u8 = null, }, /// Used when performing a many-to-many relationship through an association table. through: struct { /// Name of the join table. table: []const u8, /// The local foreign key name. /// Use the default key name of the current model. foreignKey: ?[]const u8 = null, /// The foreign key name in the join table. joinForeignKey: []const u8, /// The model key name in the join table. joinModelKey: []const u8, /// Associated model key name. /// Use the default key name of the associated model. modelKey: ?[]const u8 = null, }, }; /// Make a "one to many" or "many to many" relationship. pub fn many(comptime fromRepo: anytype, comptime toRepo: anytype, comptime config: ManyConfiguration) type { return typedMany( fromRepo.ModelType, fromRepo.TableType, fromRepo.config, toRepo.ModelType, toRepo.TableType, toRepo.config, config, ); } /// Internal implementation of a new "one to many" or "many to many" relationship. pub fn typedMany( comptime FromModel: type, comptime FromTable: type, comptime fromRepositoryConfig: repository.RepositoryConfiguration(FromModel, FromTable), comptime ToModel: type, comptime ToTable: type, comptime toRepositoryConfig: repository.RepositoryConfiguration(ToModel, ToTable), comptime config: ManyConfiguration) type { return struct { /// Relationship implementation. pub fn Implementation(field: []const u8) type { // Get foreign key from relationship 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 relationship 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], }; _ = modelKey; const FromKeyType = std.meta.fields(FromModel)[std.meta.fieldIndex(FromModel, fromRepositoryConfig.key[0]).?].type; const QueryType = _query.RepositoryQuery(ToModel, ToTable, toRepositoryConfig, null, struct { __zrm_relationship_key: FromKeyType, }); const alias = "relationships." ++ 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_relationship_key\"", .params = &[0]_sql.RawQueryParameter{}, }); // Build WHERE condition. try query.whereIn(FromKeyType, "\"" ++ toRepositoryConfig.table ++ "\".\"" ++ foreignKey ++ "\"", modelsIds); }, .through => |through| { // Add SELECT. query.select(.{ .sql = baseSelect ++ ", \"" ++ prefix ++ "pivot" ++ "\".\"" ++ through.joinModelKey ++ "\" AS \"__zrm_relationship_key\"", .params = &[0]_sql.RawQueryParameter{}, }); query.join(.{ .sql = "INNER JOIN \"" ++ through.table ++ "\" AS \"" ++ prefix ++ "pivot" ++ "\" " ++ "ON \"" ++ toRepositoryConfig.table ++ "\"." ++ foreignKey ++ " = " ++ "\"" ++ prefix ++ "pivot" ++ "\"." ++ through.joinForeignKey, .params = &[0]_sql.RawQueryParameter{}, }); // Build WHERE condition. try query.whereIn(FromKeyType, "\"" ++ prefix ++ "pivot" ++ "\".\"" ++ through.joinModelKey ++ "\"", modelsIds); }, } return query; // Return built query. } /// Build the "many" generic relationship. pub fn relationship(_: Self) Relationship { return .{ ._interface = .{ .repositoryConfiguration = &toRepositoryConfig, .buildQuery = buildQuery, }, .Model = ToModel, .TableShape = ToTable, .field = field, .alias = alias, .prefix = prefix, .QueryType = QueryType, .inlineMapping = false, .join = undefined, .select = genSelect(), }; } }; } }; } /// Configure a "one to one" relationship. pub const OneConfiguration = union(enum) { /// Direct one-to-one relationship using a local foreign key. direct: struct { /// The local foreign key name. foreignKey: []const u8, /// Associated model key name. /// Use the default key name of the associated model. modelKey: ?[]const u8 = null, }, /// Reverse one-to-one relationship using distant foreign key. reverse: struct { /// The distant foreign key name. /// Use the default key name of the related model. foreignKey: ?[]const u8 = null, /// Current model key name. /// Use the default key name of the current model. modelKey: ?[]const u8 = null, }, /// Used when performing a one-to-one relationship through an association table. through: struct { /// Name of the join table. table: []const u8, /// The local foreign key name. /// Use the default key name of the current model. foreignKey: ?[]const u8 = null, /// The foreign key name in the join table. joinForeignKey: []const u8, /// The model key name in the join table. joinModelKey: []const u8, /// Associated model key name. /// Use the default key name of the associated model. modelKey: ?[]const u8 = null, }, }; /// Make a "one to one" relationship. pub fn one(comptime fromRepo: anytype, comptime toRepo: anytype, comptime config: OneConfiguration) type { return typedOne( fromRepo.ModelType, fromRepo.TableType, fromRepo.config, toRepo.ModelType, toRepo.TableType, toRepo.config, config, ); } /// Internal implementation of a new "one to one" relationship. fn typedOne( comptime FromModel: type, comptime FromTable: type, comptime fromRepositoryConfig: repository.RepositoryConfiguration(FromModel, FromTable), comptime ToModel: type, comptime ToTable: type, comptime toRepositoryConfig: repository.RepositoryConfiguration(ToModel, ToTable), comptime config: OneConfiguration) type { return struct { 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_relationship_key: FromKeyType, }); // Get foreign key from relationship config or repository config. const foreignKey = switch (config) { .direct => |direct| direct.foreignKey, .reverse => |reverse| if (reverse.foreignKey) |_foreignKey| _foreignKey else toRepositoryConfig.key[0], .through => |through| if (through.foreignKey) |_foreignKey| _foreignKey else fromRepositoryConfig.key[0], }; // Get model key from relationship 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 fromRepositoryConfig.key[0], .through => |through| if (through.modelKey) |_modelKey| _modelKey else toRepositoryConfig.key[0], }; const alias = "relationships." ++ 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_relationship_key\"", .params = &[0]_sql.RawQueryParameter{}, }); query.join((_sql.RawQuery{ .sql = "INNER JOIN \"" ++ fromRepositoryConfig.table ++ "\" AS \"" ++ prefix ++ "related" ++ "\" ON " ++ "\"" ++ toRepositoryConfig.table ++ "\"." ++ modelKey ++ " = \"" ++ prefix ++ "related" ++ "\"." ++ foreignKey, .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_relationship_key\"", .params = &[0]_sql.RawQueryParameter{}, }); // Build WHERE condition. try query.whereIn(FromKeyType, "\"" ++ toRepositoryConfig.table ++ "\".\"" ++ foreignKey ++ "\"", modelsIds); }, .through => |through| { // Add SELECT. query.select(.{ .sql = baseSelect ++ ", \"" ++ prefix ++ "pivot" ++ "\".\"" ++ through.joinForeignKey ++ "\" AS \"__zrm_relationship_key\"", .params = &[0]_sql.RawQueryParameter{}, }); query.join(.{ .sql = "INNER JOIN \"" ++ through.table ++ "\" AS \"" ++ prefix ++ "pivot" ++ "\" ON " ++ "\"" ++ toRepositoryConfig.table ++ "\"." ++ modelKey ++ " = " ++ "\"" ++ prefix ++ "pivot" ++ "\"." ++ through.joinModelKey, .params = &[0]_sql.RawQueryParameter{}, }); // Build WHERE condition. try query.whereIn(FromKeyType, "\"" ++ prefix ++ "pivot" ++ "\".\"" ++ through.joinForeignKey ++ "\"", modelsIds); }, } // Return built query. return query; } /// Build the "one" generic relationship. pub fn relationship(_: Self) Relationship { 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 relationship interface. pub const Relationship = struct { const Self = @This(); _interface: struct { repositoryConfiguration: *const 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 relationship. alias: []const u8, /// Prefix of fields of the relationship. prefix: []const u8, /// Type of a query of the related models. QueryType: type, /// Set if relationship 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 relationship data. /// Is always used when inline mapping is not possible, but also when loading relationships lazily. pub fn buildQuery(self: Self, models: []const *anyopaque, allocator: std.mem.Allocator, connector: _database.Connector) !*anyopaque { return self._interface.buildQuery(models, allocator, connector); } /// 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 relationship. pub const Eager = struct { /// The relationship to eager load. relationship: Relationship, /// Subrelationships to eager load. with: []const Eager, };