From 24989603a3f111078779bbe3ce31a614ab3ee949 Mon Sep 17 00:00:00 2001 From: Madeorsk Date: Sun, 24 Nov 2024 01:53:00 +0100 Subject: [PATCH] Handle inline relations in result parsing. + Parse inline relations when parsing a query result. + Implement a custom pg.Mapper to pass the row manually. --- src/postgresql.zig | 172 +++++++++++++++++++++++++++++++++++++++++++-- src/query.zig | 4 +- src/relations.zig | 82 +++++++++++++-------- src/result.zig | 89 +++++++++++++++++++++-- 4 files changed, 305 insertions(+), 42 deletions(-) diff --git a/src/postgresql.zig b/src/postgresql.zig index 677fa2b..bb63ae7 100644 --- a/src/postgresql.zig +++ b/src/postgresql.zig @@ -59,8 +59,89 @@ pub fn handleRawPostgresqlError(err: anyerror, connection: *pg.Conn) anyerror { } } +fn isSlice(comptime T: type) ?type { + switch(@typeInfo(T)) { + .Pointer => |ptr| { + if (ptr.size != .Slice) { + @compileError("cannot get value of type " ++ @typeName(T)); + } + return if (ptr.child == u8) null else ptr.child; + }, + .Optional => |opt| return isSlice(opt.child), + else => return null, + } +} + +fn mapValue(comptime T: type, value: T, allocator: std.mem.Allocator) !T { + switch (@typeInfo(T)) { + .Optional => |opt| { + if (value) |v| { + return try mapValue(opt.child, v, allocator); + } + return null; + }, + else => {}, + } + + if (T == []u8 or T == []const u8) { + return try allocator.dupe(u8, value); + } + + if (std.meta.hasFn(T, "pgzMoveOwner")) { + return value.pgzMoveOwner(allocator); + } + + return value; +} + +fn rowMapColumn(self: *const pg.Row, field: *const std.builtin.Type.StructField, optional_column_index: ?usize, allocator: ?std.mem.Allocator) !field.type { + const T = field.type; + const column_index = optional_column_index orelse { + if (field.default_value) |dflt| { + return @as(*align(1) const field.type, @ptrCast(dflt)).*; + } + return error.FieldColumnMismatch; + }; + + if (comptime isSlice(T)) |S| { + const slice = blk: { + if (@typeInfo(T) == .Optional) { + break :blk self.get(?pg.Iterator(S), column_index) orelse return null; + } else { + break :blk self.get(pg.Iterator(S), column_index); + } + }; + return try slice.alloc(allocator orelse return error.AllocatorRequiredForSliceMapping); + } + + const value = self.get(field.type, column_index); + const a = allocator orelse return value; + return mapValue(T, value, a); +} + +pub fn PgMapper(comptime T: type) type { + return struct { + result: *pg.Result, + allocator: ?std.mem.Allocator, + column_indexes: [std.meta.fields(T).len]?usize, + + const Self = @This(); + + pub fn next(self: *const Self, row: *pg.Row) !?T { + var value: T = undefined; + + const allocator = self.allocator; + inline for (std.meta.fields(T), self.column_indexes) |field, optional_column_index| { + //TODO I must reimplement row.mapColumn because it's not public :-( + @field(value, field.name) = try rowMapColumn(row, &field, optional_column_index, allocator); + } + return value; + } + }; +} + /// Make a PostgreSQL result mapper with the given prefix, if there is one. -pub fn makeMapper(comptime T: type, result: *pg.Result, allocator: std.mem.Allocator, optionalPrefix: ?[]const u8) !pg.Mapper(T) { +pub fn makeMapper(comptime T: type, result: *pg.Result, allocator: std.mem.Allocator, optionalPrefix: ?[]const u8) !PgMapper(T) { var column_indexes: [std.meta.fields(T).len]?usize = undefined; inline for (std.meta.fields(T), 0..) |field, i| { @@ -80,21 +161,91 @@ 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; + // Build relations mappers container type. + const RelationsMappersType = comptime typeBuilder: { + if (inlineRelations) |_inlineRelations| { + // Make a field for each relation. + var fields: [_inlineRelations.len]std.builtin.Type.StructField = undefined; + + 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); + + field.* = .{ + .name = relation.field ++ [0:0]u8{}, + .type = relationFieldType, + .default_value = null, + .is_comptime = false, + .alignment = @alignOf(relationFieldType), + }; + } + + // Build type with one field for each relation. + break :typeBuilder @Type(std.builtin.Type{ + .Struct = .{ + .layout = std.builtin.Type.ContainerLayout.auto, + .fields = &fields, + .decls = &[0]std.builtin.Type.Declaration{}, + .is_tuple = false, + }, + }); + } + + // Build default empty type. + break :typeBuilder @Type(std.builtin.Type{ + .Struct = .{ + .layout = std.builtin.Type.ContainerLayout.auto, + .fields = &[0]std.builtin.Type.StructField{}, + .decls = &[0]std.builtin.Type.Declaration{}, + .is_tuple = false, + }, + }); + }; + return struct { const Self = @This(); + /// PostgreSQL implementation of the query result reader instance. pub const Instance = struct { /// Main object mapper. - mainMapper: pg.Mapper(TableShape) = undefined, + mainMapper: PgMapper(TableShape) = undefined, + relationsMappers: RelationsMappersType = undefined, - fn next(opaqueSelf: *anyopaque) !?TableShape { //TODO inline relations. + fn next(opaqueSelf: *anyopaque) !?_result.TableWithRelations(TableShape, inlineRelations) { const self: *Instance = @ptrCast(@alignCast(opaqueSelf)); - return try self.mainMapper.next(); + + // Try to get the next row. + var row: pg.Row = try self.mainMapper.result.next() orelse return null; + + // Get main table result. + const mainTable = try self.mainMapper.next(&row) orelse return null; + + // Initialize the result. + var result: _result.TableWithRelations(TableShape, inlineRelations) = undefined; + + // Copy each basic table field. + inline for (std.meta.fields(TableShape)) |field| { + @field(result, field.name) = @field(mainTable, field.name); + } + + if (inlineRelations) |_inlineRelations| { + // For each relation, retrieve its value and put it in the result. + inline for (_inlineRelations) |relation| { + //TODO detect null relation. + @field(result, relation.field) = try @field(self.relationsMappers, relation.field).next(&row); + } + } + + return result; // Return built result. } + /// Get the generic reader instance instance. pub fn instance(self: *Instance, allocator: std.mem.Allocator) InstanceInterface { return .{ .__interface = .{ @@ -115,9 +266,22 @@ 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 (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 ++ "."); + } + } + return self.instance.instance(allocator); } + /// Get the generic reader instance. pub fn reader(self: *Self) _result.QueryResultReader(TableShape, inlineRelations) { return .{ ._interface = .{ diff --git a/src/query.zig b/src/query.zig index 7ad726c..1a5836b 100644 --- a/src/query.zig +++ b/src/query.zig @@ -43,8 +43,8 @@ pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime for (_with) |relation| { // For each relation, determine if it's inline or not. - var tt = relation.relation{}; - const relationInstance = tt.relation(); + var relationImpl = relation.relation{}; + const relationInstance = relationImpl.relation(); if (relationInstance.inlineMapping()) { // Add the current relation to inline relations. inlineRelations = @ptrCast(@constCast(_comptime.append(inlineRelations, relation))); diff --git a/src/relations.zig b/src/relations.zig index b59d259..0bc1223 100644 --- a/src/relations.zig +++ b/src/relations.zig @@ -67,6 +67,10 @@ pub fn typedMany( return struct { const Self = @This(); + fn getRepositoryConfiguration(_: *anyopaque) repository.RepositoryConfiguration(ToModel, ToTable) { + return toRepositoryConfig; + } + fn inlineMapping(_: *anyopaque) bool { return false; } @@ -109,11 +113,12 @@ pub fn typedMany( } } - pub fn relation(self: *Self) Relation { + pub fn relation(self: *Self) Relation(ToModel, ToTable) { return .{ ._interface = .{ .instance = self, + .getRepositoryConfiguration = getRepositoryConfiguration, .inlineMapping = inlineMapping, .genJoin = genJoin, .genSelect = genSelect, @@ -199,6 +204,10 @@ fn typedOne( return struct { const Self = @This(); + fn getRepositoryConfiguration(_: *anyopaque) repository.RepositoryConfiguration(ToModel, ToTable) { + return toRepositoryConfig; + } + fn inlineMapping(_: *anyopaque) bool { return true; } @@ -267,11 +276,12 @@ fn typedOne( } } - pub fn relation(self: *Self) Relation { + pub fn relation(self: *Self) Relation(ToModel, ToTable) { return .{ ._interface = .{ .instance = self, + .getRepositoryConfiguration = getRepositoryConfiguration, .inlineMapping = inlineMapping, .genJoin = genJoin, .genSelect = genSelect, @@ -282,42 +292,52 @@ fn typedOne( }; } - /// Generic model relation interface. -pub const Relation = struct { - const Self = @This(); +pub fn Relation(comptime ToModel: type, comptime ToTable: type) type { + return struct { + const Self = @This(); - _interface: struct { - instance: *anyopaque, + pub const Model = ToModel; + pub const TableShape = 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, - buildQuery: *const fn (self: *anyopaque, models: []const anyopaque, query: *anyopaque) anyerror!void, - }, + _interface: struct { + instance: *anyopaque, - /// 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); - } + 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, + buildQuery: *const fn (self: *anyopaque, models: []const anyopaque, query: *anyopaque) anyerror!void, + }, - /// 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); - } + /// Read the related model repository configuration. + pub fn getRepositoryConfiguration(self: Self) repository.RepositoryConfiguration(ToModel, ToTable) { + return self._interface.getRepositoryConfiguration(self._interface.instance); + } - /// 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); - } + /// 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); + } - /// 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); - } -}; + /// 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); + } + + /// 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); + } + }; +} /// A model relation object. diff --git a/src/result.zig b/src/result.zig index e90225e..9a690be 100644 --- a/src/result.zig +++ b/src/result.zig @@ -3,9 +3,73 @@ const zollections = @import("zollections"); const _repository = @import("repository.zig"); const _relations = @import("relations.zig"); +/// Type of a retrieved table data, with its retrieved relations. +pub fn TableWithRelations(comptime TableShape: 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; + // 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| { + // 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 + }, + }); + + // 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), + }; + } + + // Build the new type. + return @Type(std.builtin.Type{ + .Struct = .{ + .layout = tableType.Struct.layout, + .fields = &fields, + .decls = tableType.Struct.decls, + .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 optionalRelations: ?[]const _relations.ModelRelation, value: TableWithRelations(TableShape, 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 inlineRelations: ?[]const _relations.ModelRelation) type { - _ = inlineRelations; return struct { const Self = @This(); @@ -13,12 +77,12 @@ pub fn QueryResultReader(comptime TableShape: type, comptime inlineRelations: ?[ pub const Instance = struct { __interface: struct { instance: *anyopaque, - next: *const fn (self: *anyopaque) anyerror!?TableShape, //TODO inline relations. + next: *const fn (self: *anyopaque) anyerror!?TableWithRelations(TableShape, inlineRelations), }, allocator: std.mem.Allocator, - pub fn next(self: Instance) !?TableShape { + pub fn next(self: Instance) !?TableWithRelations(TableShape, inlineRelations) { return self.__interface.next(self.__interface.instance); } }; @@ -55,8 +119,23 @@ pub fn ResultMapper(comptime Model: type, comptime TableShape: type, comptime re while (try reader.next()) |rawModel| { // Parse each raw model from the reader. const model = try allocator.create(Model); - model.* = try repositoryConfig.fromSql(rawModel); - //TODO inline relations. + model.* = try repositoryConfig.fromSql(toTableShape(TableShape, inlineRelations, rawModel)); + + // Map inline relations. + 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) + else null + ); + } + } + try models.append(model); }