411 lines
15 KiB
Zig
411 lines
15 KiB
Zig
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,
|
|
};
|