zrm/src/result.zig

225 lines
8.3 KiB
Zig
Raw Normal View History

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 MetadataShape: ?type, comptime optionalRelations: ?[]const _relations.Relation) 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 + (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..(tableType.Struct.fields.len+relations.len)]) |relation, *field| {
// Get relation field type (optional TableShape of the related value).
const relationFieldType = @Type(std.builtin.Type{
.Optional = .{
.child = relation.TableShape
},
});
// Create the new field from relation data.
field.* = std.builtin.Type.StructField{
.name = relation.field ++ [0:0]u8{},
.type = relationFieldType,
.default_value = null,
.is_comptime = false,
.alignment = @alignOf(relationFieldType),
};
}
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 = .{
.layout = tableType.Struct.layout,
.fields = &fields,
.decls = &[0]std.builtin.Type.Declaration{},
.is_tuple = tableType.Struct.is_tuple,
.backing_integer = tableType.Struct.backing_integer,
},
});
} else {
return TableShape;
}
}
/// 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.Relation, value: TableWithRelations(TableShape, MetadataShape, optionalRelations)) TableShape {
if (optionalRelations) |_| {
// Make a structure of TableShape type.
var tableValue: TableShape = undefined;
// Copy all fields of the table shape in the new structure.
inline for (std.meta.fields(TableShape)) |field| {
@field(tableValue, field.name) = @field(value, field.name);
}
// Return the simplified structure.
return tableValue;
} else {
// No relations, it should already be of type TableShape.
return value;
}
}
/// Generic interface of a query result reader.
pub fn QueryResultReader(comptime TableShape: type, comptime MetadataShape: ?type, comptime inlineRelations: ?[]const _relations.Relation) type {
return struct {
const Self = @This();
/// Generic interface of a query result reader instance.
pub const Instance = struct {
__interface: struct {
instance: *anyopaque,
next: *const fn (self: *anyopaque) anyerror!?TableWithRelations(TableShape, MetadataShape, inlineRelations),
},
allocator: std.mem.Allocator,
pub fn next(self: Instance) !?TableWithRelations(TableShape, MetadataShape, inlineRelations) {
return self.__interface.next(self.__interface.instance);
}
};
_interface: struct {
instance: *anyopaque,
init: *const fn (self: *anyopaque, allocator: std.mem.Allocator) anyerror!Instance,
},
/// Initialize a reader instance.
pub fn init(self: Self, allocator: std.mem.Allocator) !Instance {
return self._interface.init(self._interface.instance, allocator);
}
};
}
/// 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.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) {
// 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);
// Initialize query result reader.
const reader = try queryReader.init(mapperArena.allocator());
// Initialize models list.
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(ResultType);
(if (withMetadata) model.model else model.*) = try repositoryConfig.fromSql(toTableShape(TableShape, MetadataShape, inlineRelations, rawModel));
// Map inline relations.
if (inlineRelations) |_inlineRelations| {
// If there are loaded inline relations, map them to the result.
inline for (_inlineRelations) |relation| {
// Set the read inline relation value.
@field(model.*, relation.field) = (
if (@field(rawModel, relation.field)) |relationVal|
try relation.repositoryConfiguration().fromSql(relationVal)
else null
);
}
}
if (withMetadata) {
// Set model metadata.
model.metadata = rawModel._zrm_metadata;
}
try models.append(model);
}
if (relations) |relationsToLoad| {
inline for (relationsToLoad) |relation| {
// Build query for the relation to get.
const query: *relation.QueryType = @ptrCast(@alignCast(
try relation.buildQuery(@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(ResultType).init(allocator,
zollections.Collection(ResultType).init(allocator, try models.toOwnedSlice()),
mapperArena,
);
}
};
}