2024-10-16 12:01:16 +02:00
|
|
|
const std = @import("std");
|
|
|
|
const pg = @import("pg");
|
|
|
|
const zollections = @import("zollections");
|
|
|
|
const errors = @import("errors.zig");
|
2024-10-22 14:09:02 +02:00
|
|
|
const database = @import("database.zig");
|
2024-10-16 12:01:16 +02:00
|
|
|
const postgresql = @import("postgresql.zig");
|
|
|
|
const _sql = @import("sql.zig");
|
2024-11-22 15:40:10 +01:00
|
|
|
const _conditions = @import("conditions.zig");
|
|
|
|
const relations = @import("relations.zig");
|
2024-10-16 12:01:16 +02:00
|
|
|
const repository = @import("repository.zig");
|
2024-11-22 22:36:51 +01:00
|
|
|
const _comptime = @import("comptime.zig");
|
2024-11-23 18:18:41 +01:00
|
|
|
const _result = @import("result.zig");
|
2024-11-22 15:40:10 +01:00
|
|
|
|
2024-10-16 12:01:16 +02:00
|
|
|
/// Repository query configuration structure.
|
|
|
|
pub const RepositoryQueryConfiguration = struct {
|
2024-11-22 15:40:10 +01:00
|
|
|
select: ?_sql.RawQuery = null,
|
|
|
|
join: ?_sql.RawQuery = null,
|
|
|
|
where: ?_sql.RawQuery = null,
|
2024-11-22 22:36:51 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
/// Compiled relations structure.
|
|
|
|
const CompiledRelations = struct {
|
2024-11-25 18:41:29 +01:00
|
|
|
inlineRelations: []relations.Relation,
|
|
|
|
otherRelations: []relations.Relation,
|
2024-11-22 22:36:51 +01:00
|
|
|
inlineSelect: []const u8,
|
|
|
|
inlineJoins: []const u8,
|
2024-10-16 12:01:16 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
/// Repository models query manager.
|
|
|
|
/// Manage query string build and its execution.
|
2024-11-25 18:41:29 +01:00
|
|
|
pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime repositoryConfig: repository.RepositoryConfiguration(Model, TableShape), comptime with: ?[]const relations.Relation, comptime MetadataShape: ?type) type {
|
2024-11-22 22:36:51 +01:00
|
|
|
const compiledRelations = comptime compile: {
|
|
|
|
// Inline relations list.
|
2024-11-25 18:41:29 +01:00
|
|
|
var inlineRelations: []relations.Relation = &[0]relations.Relation{};
|
2024-11-25 13:02:59 +01:00
|
|
|
// Other relations list.
|
2024-11-25 18:41:29 +01:00
|
|
|
var otherRelations: []relations.Relation = &[0]relations.Relation{};
|
2024-11-22 22:36:51 +01:00
|
|
|
|
|
|
|
if (with) |_with| {
|
|
|
|
// If there are relations to eager load, prepare their query.
|
|
|
|
|
|
|
|
// Initialize inline select array.
|
|
|
|
var inlineSelect: [][]const u8 = &[0][]const u8{};
|
|
|
|
// Initialize inline joins array.
|
|
|
|
var inlineJoins: [][]const u8 = &[0][]const u8{};
|
|
|
|
|
|
|
|
for (_with) |relation| {
|
|
|
|
// For each relation, determine if it's inline or not.
|
2024-11-25 18:41:29 +01:00
|
|
|
if (relation.inlineMapping) {
|
2024-11-22 22:36:51 +01:00
|
|
|
// Add the current relation to inline relations.
|
|
|
|
inlineRelations = @ptrCast(@constCast(_comptime.append(inlineRelations, relation)));
|
|
|
|
|
|
|
|
// Generate selected columns for the relation.
|
2024-11-25 18:41:29 +01:00
|
|
|
inlineSelect = @ptrCast(@constCast(_comptime.append(inlineSelect, relation.select)));
|
2024-11-22 22:36:51 +01:00
|
|
|
// Generate joined table for the relation.
|
2024-11-25 18:41:29 +01:00
|
|
|
inlineJoins = @ptrCast(@constCast(_comptime.append(inlineJoins, relation.join)));
|
2024-11-25 13:02:59 +01:00
|
|
|
} else {
|
|
|
|
// Add the current relation to other relations.
|
|
|
|
otherRelations = @ptrCast(@constCast(_comptime.append(otherRelations, relation)));
|
2024-11-22 22:36:51 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
break :compile CompiledRelations{
|
2024-11-23 18:18:41 +01:00
|
|
|
.inlineRelations = inlineRelations,
|
2024-11-25 13:02:59 +01:00
|
|
|
.otherRelations = otherRelations,
|
2024-11-22 22:36:51 +01:00
|
|
|
.inlineSelect = if (inlineSelect.len > 0) ", " ++ _comptime.join(", ", inlineSelect) else "",
|
|
|
|
.inlineJoins = if (inlineJoins.len > 0) " " ++ _comptime.join(" ", inlineJoins) else "",
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
break :compile CompiledRelations{
|
2024-11-25 18:41:29 +01:00
|
|
|
.inlineRelations = &[0]relations.Relation{},
|
|
|
|
.otherRelations = &[0]relations.Relation{},
|
2024-11-22 22:36:51 +01:00
|
|
|
.inlineSelect = "",
|
|
|
|
.inlineJoins = "",
|
|
|
|
};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Pre-compute SQL buffer.
|
2024-11-22 15:40:10 +01:00
|
|
|
const fromClause = " FROM \"" ++ repositoryConfig.table ++ "\"";
|
2024-11-22 22:36:51 +01:00
|
|
|
const defaultSelectSql = "\"" ++ repositoryConfig.table ++ "\".*" ++ compiledRelations.inlineSelect;
|
|
|
|
const defaultJoin = compiledRelations.inlineJoins;
|
2024-11-22 15:40:10 +01:00
|
|
|
|
|
|
|
// Model key type.
|
|
|
|
const KeyType = repository.ModelKeyType(Model, TableShape, repositoryConfig);
|
2024-10-16 12:01:16 +02:00
|
|
|
|
|
|
|
return struct {
|
|
|
|
const Self = @This();
|
|
|
|
|
2024-11-23 18:18:41 +01:00
|
|
|
/// Result mapper type.
|
2024-11-25 13:02:59 +01:00
|
|
|
pub const ResultMapper = _result.ResultMapper(Model, TableShape, MetadataShape, repositoryConfig, compiledRelations.inlineRelations, compiledRelations.otherRelations);
|
2024-11-23 18:18:41 +01:00
|
|
|
|
2024-10-16 12:01:16 +02:00
|
|
|
arena: std.heap.ArenaAllocator,
|
2024-10-22 14:09:02 +02:00
|
|
|
connector: database.Connector,
|
|
|
|
connection: *database.Connection = undefined,
|
2024-10-16 12:01:16 +02:00
|
|
|
queryConfig: RepositoryQueryConfiguration,
|
|
|
|
|
2024-11-22 15:40:10 +01:00
|
|
|
query: ?_sql.RawQuery = null,
|
2024-10-16 12:01:16 +02:00
|
|
|
sql: ?[]const u8 = null,
|
|
|
|
|
|
|
|
/// Set selected columns.
|
2024-11-22 15:40:10 +01:00
|
|
|
pub fn select(self: *Self, _select: _sql.RawQuery) void {
|
2024-10-16 12:01:16 +02:00
|
|
|
self.queryConfig.select = _select;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Set selected columns for SELECT clause.
|
|
|
|
pub fn selectColumns(self: *Self, _select: []const []const u8) !void {
|
|
|
|
if (_select.len == 0) {
|
|
|
|
return errors.AtLeastOneSelectionRequired;
|
|
|
|
}
|
|
|
|
|
|
|
|
self.select(.{
|
|
|
|
// Join selected columns.
|
|
|
|
.sql = std.mem.join(self.arena.allocator(), ", ", _select),
|
2024-11-22 15:40:10 +01:00
|
|
|
.params = &[_]_sql.RawQueryParameter{}, // No parameters.
|
2024-10-16 12:01:16 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Set JOIN clause.
|
2024-11-22 15:40:10 +01:00
|
|
|
pub fn join(self: *Self, _join: _sql.RawQuery) void {
|
2024-10-16 12:01:16 +02:00
|
|
|
self.queryConfig.join = _join;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Set WHERE conditions.
|
2024-11-22 15:40:10 +01:00
|
|
|
pub fn where(self: *Self, _where: _sql.RawQuery) void {
|
2024-10-16 12:01:16 +02:00
|
|
|
self.queryConfig.where = _where;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Create a new condition builder.
|
2024-11-22 15:40:10 +01:00
|
|
|
pub fn newCondition(self: *Self) _conditions.Builder {
|
|
|
|
return _conditions.Builder.init(self.arena.allocator());
|
2024-10-16 12:01:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Set a WHERE value condition.
|
|
|
|
pub fn whereValue(self: *Self, comptime ValueType: type, comptime _column: []const u8, comptime operator: []const u8, _value: ValueType) !void {
|
|
|
|
self.where(
|
2024-11-22 15:40:10 +01:00
|
|
|
try _conditions.value(ValueType, self.arena.allocator(), _column, operator, _value)
|
2024-10-16 12:01:16 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Set a WHERE column condition.
|
|
|
|
pub fn whereColumn(self: *Self, comptime _column: []const u8, comptime operator: []const u8, comptime _valueColumn: []const u8) !void {
|
|
|
|
self.where(
|
2024-11-22 15:40:10 +01:00
|
|
|
try _conditions.column(self.arena.allocator(), _column, operator, _valueColumn)
|
2024-10-16 12:01:16 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Set a WHERE IN condition.
|
|
|
|
pub fn whereIn(self: *Self, comptime ValueType: type, comptime _column: []const u8, _value: []const ValueType) !void {
|
|
|
|
self.where(
|
2024-11-22 15:40:10 +01:00
|
|
|
try _conditions.in(ValueType, self.arena.allocator(), _column, _value)
|
2024-10-16 12:01:16 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-11-22 15:40:10 +01:00
|
|
|
/// Set a WHERE from model key(s).
|
|
|
|
/// For simple keys: modelKey type must match the type of its corresponding field.
|
|
|
|
/// modelKey can be an array / slice of keys.
|
|
|
|
/// For composite keys: modelKey must be a struct with all the keys, matching the type of their corresponding field.
|
|
|
|
/// modelKey can be an array / slice of these structs.
|
|
|
|
pub fn whereKey(self: *Self, modelKey: anytype) !void {
|
|
|
|
if (repositoryConfig.key.len == 1) {
|
|
|
|
// Find key name and its type.
|
|
|
|
const keyName = repositoryConfig.key[0];
|
|
|
|
const keyType = std.meta.fields(TableShape)[std.meta.fieldIndex(TableShape, keyName).?].type;
|
|
|
|
|
|
|
|
// Accept arrays / slices of keys, and simple keys.
|
|
|
|
switch (@typeInfo(@TypeOf(modelKey))) {
|
|
|
|
.Pointer => |ptr| {
|
|
|
|
switch (ptr.size) {
|
|
|
|
.One => {
|
|
|
|
switch (@typeInfo(ptr.child)) {
|
|
|
|
// Add a whereIn with the array.
|
|
|
|
.Array => {
|
|
|
|
if (ptr.child == u8)
|
|
|
|
// If the child is a string, use it as a simple value.
|
|
|
|
try self.whereValue(KeyType, keyName, "=", modelKey)
|
|
|
|
else
|
|
|
|
// Otherwise, use it as an array.
|
|
|
|
try self.whereIn(keyType, keyName, modelKey);
|
|
|
|
},
|
|
|
|
// Add a simple condition with the pointed value.
|
|
|
|
else => try self.whereValue(keyType, keyName, "=", modelKey.*),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
// Add a whereIn with the slice.
|
|
|
|
else => {
|
|
|
|
if (ptr.child == u8)
|
|
|
|
// If the child is a string, use it as a simple value.
|
|
|
|
try self.whereValue(KeyType, keyName, "=", modelKey)
|
|
|
|
else
|
|
|
|
// Otherwise, use it as an array.
|
|
|
|
try self.whereIn(keyType, keyName, modelKey);
|
|
|
|
},
|
|
|
|
}
|
|
|
|
},
|
|
|
|
// Add a simple condition with the given value.
|
|
|
|
else => try self.whereValue(keyType, keyName, "=", modelKey),
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Accept arrays / slices of keys, and simple keys.
|
|
|
|
// Uniformize modelKey parameter to a slice.
|
|
|
|
const modelKeysList: []const KeyType = switch (@typeInfo(@TypeOf(modelKey))) {
|
|
|
|
.Pointer => |ptr| switch (ptr.size) {
|
|
|
|
.One => switch (@typeInfo(ptr.child)) {
|
|
|
|
// Already an array.
|
|
|
|
.Array => @as([]const KeyType, modelKey),
|
|
|
|
// Convert the pointer to an array.
|
|
|
|
else => &[1]KeyType{@as(KeyType, modelKey.*)},
|
|
|
|
},
|
|
|
|
// Already a slice.
|
|
|
|
else => @as([]const KeyType, modelKey),
|
|
|
|
},
|
|
|
|
// Convert the value to an array.
|
|
|
|
else => &[1]KeyType{@as(KeyType, modelKey)},
|
|
|
|
};
|
|
|
|
|
|
|
|
// Initialize keys conditions list.
|
|
|
|
const conditions: []_sql.RawQuery = try self.arena.allocator().alloc(_sql.RawQuery, modelKeysList.len);
|
|
|
|
defer self.arena.allocator().free(conditions);
|
|
|
|
|
|
|
|
// For each model key, add its conditions.
|
|
|
|
for (modelKeysList, conditions) |_modelKey, *condition| {
|
|
|
|
condition.* = try self.newCondition().@"and"(
|
|
|
|
&try buildCompositeKeysConditions(TableShape, repositoryConfig.key, self.newCondition(), _modelKey)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set WHERE conditions in the query with all keys conditions.
|
|
|
|
self.where(try self.newCondition().@"or"(conditions));
|
2024-10-16 12:01:16 +02:00
|
|
|
}
|
2024-11-22 15:40:10 +01:00
|
|
|
}
|
2024-10-16 12:01:16 +02:00
|
|
|
|
2024-11-22 15:40:10 +01:00
|
|
|
/// Build SQL query.
|
|
|
|
pub fn buildSql(self: *Self) !void {
|
|
|
|
// Build the full SQL query from all its parts.
|
|
|
|
const sqlQuery = _sql.RawQuery{
|
|
|
|
.sql = try std.mem.join(self.arena.allocator(), "", &[_][]const u8{
|
|
|
|
"SELECT ", if (self.queryConfig.select) |_select| _select.sql else defaultSelectSql,
|
|
|
|
fromClause,
|
2024-11-22 22:36:51 +01:00
|
|
|
defaultJoin,
|
2024-11-22 15:40:10 +01:00
|
|
|
if (self.queryConfig.join) |_| " " else "",
|
|
|
|
if (self.queryConfig.join) |_join| _join.sql else "",
|
|
|
|
if (self.queryConfig.where) |_| " WHERE " else "",
|
|
|
|
if (self.queryConfig.where) |_where| _where.sql else "",
|
|
|
|
";",
|
|
|
|
}),
|
|
|
|
.params = try std.mem.concat(self.arena.allocator(), _sql.RawQueryParameter, &[_][]const _sql.RawQueryParameter{
|
|
|
|
if (self.queryConfig.select) |_select| _select.params else &[0]_sql.RawQueryParameter{},
|
|
|
|
if (self.queryConfig.join) |_join| _join.params else &[0]_sql.RawQueryParameter{},
|
|
|
|
if (self.queryConfig.where) |_where| _where.params else &[0]_sql.RawQueryParameter{},
|
|
|
|
})
|
|
|
|
};
|
2024-10-16 12:01:16 +02:00
|
|
|
|
|
|
|
// Save built SQL query.
|
2024-11-22 15:40:10 +01:00
|
|
|
self.query = sqlQuery;
|
|
|
|
self.sql = try sqlQuery.build(self.arena.allocator());
|
2024-10-16 12:01:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Execute the built query.
|
2024-10-22 14:09:02 +02:00
|
|
|
fn execQuery(self: *Self) !*pg.Result {
|
|
|
|
// Get the connection to the database.
|
|
|
|
self.connection = try self.connector.getConnection();
|
|
|
|
errdefer self.connection.release();
|
2024-10-16 12:01:16 +02:00
|
|
|
|
|
|
|
// Initialize a new PostgreSQL statement.
|
2024-10-22 14:09:02 +02:00
|
|
|
var statement = try pg.Stmt.init(self.connection.connection, .{
|
2024-10-16 12:01:16 +02:00
|
|
|
.column_names = true,
|
|
|
|
.allocator = self.arena.allocator(),
|
|
|
|
});
|
|
|
|
errdefer statement.deinit();
|
|
|
|
|
|
|
|
// Prepare SQL query.
|
|
|
|
statement.prepare(self.sql.?)
|
2024-10-22 14:09:02 +02:00
|
|
|
catch |err| return postgresql.handlePostgresqlError(err, self.connection, &statement);
|
2024-10-16 12:01:16 +02:00
|
|
|
|
|
|
|
// Bind query parameters.
|
2024-11-22 15:40:10 +01:00
|
|
|
postgresql.bindQueryParameters(&statement, self.query.?.params)
|
|
|
|
catch |err| return postgresql.handlePostgresqlError(err, self.connection, &statement);
|
2024-10-16 12:01:16 +02:00
|
|
|
|
|
|
|
// Execute the query and get its result.
|
|
|
|
const result = statement.execute()
|
2024-10-22 14:09:02 +02:00
|
|
|
catch |err| return postgresql.handlePostgresqlError(err, self.connection, &statement);
|
2024-10-16 12:01:16 +02:00
|
|
|
|
|
|
|
// Query executed successfully, return the result.
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2024-11-25 13:02:59 +01:00
|
|
|
/// Generic queried models retrieval.
|
|
|
|
fn _get(self: *Self, allocator: std.mem.Allocator, comptime withMetadata: bool) !repository.RepositoryResult(if (withMetadata) _result.ModelWithMetadata(Model, MetadataShape) else Model) {
|
2024-10-16 12:01:16 +02:00
|
|
|
// Build SQL query if it wasn't built.
|
|
|
|
if (self.sql) |_| {} else { try self.buildSql(); }
|
|
|
|
|
|
|
|
// Execute query and get its result.
|
2024-10-17 13:04:15 +02:00
|
|
|
var queryResult = try self.execQuery();
|
2024-10-22 14:09:02 +02:00
|
|
|
defer self.connection.release();
|
2024-10-17 13:04:15 +02:00
|
|
|
defer queryResult.deinit();
|
2024-10-16 12:01:16 +02:00
|
|
|
|
2024-10-22 21:42:00 +02:00
|
|
|
// Map query results.
|
2024-11-25 13:02:59 +01:00
|
|
|
var postgresqlReader = postgresql.QueryResultReader(TableShape, MetadataShape, compiledRelations.inlineRelations).init(queryResult);
|
|
|
|
return try ResultMapper.map(withMetadata, allocator, self.connector, postgresqlReader.reader());
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Retrieve queried models.
|
|
|
|
pub fn get(self: *Self, allocator: std.mem.Allocator) !repository.RepositoryResult(Model) {
|
|
|
|
return self._get(allocator, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Retrieved queries models with metadata.
|
|
|
|
pub fn getWithMetadata(self: *Self, allocator: std.mem.Allocator) !repository.RepositoryResult(_result.ModelWithMetadata(Model, MetadataShape)) {
|
|
|
|
if (MetadataShape) |_| {
|
|
|
|
return self._get(allocator, true);
|
|
|
|
} else {
|
|
|
|
unreachable;
|
|
|
|
}
|
2024-10-16 12:01:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Initialize a new repository query.
|
2024-10-22 14:09:02 +02:00
|
|
|
pub fn init(allocator: std.mem.Allocator, connector: database.Connector, queryConfig: RepositoryQueryConfiguration) Self {
|
2024-10-16 12:01:16 +02:00
|
|
|
return .{
|
|
|
|
// Initialize the query arena allocator.
|
|
|
|
.arena = std.heap.ArenaAllocator.init(allocator),
|
2024-10-22 14:09:02 +02:00
|
|
|
.connector = connector,
|
2024-10-16 12:01:16 +02:00
|
|
|
.queryConfig = queryConfig,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Deinitialize the repository query.
|
|
|
|
pub fn deinit(self: *Self) void {
|
2024-10-22 14:09:02 +02:00
|
|
|
// Free everything allocated for this query.
|
2024-10-16 12:01:16 +02:00
|
|
|
self.arena.deinit();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
2024-11-22 15:40:10 +01:00
|
|
|
|
|
|
|
/// Build conditions for given composite keys, with a model key structure.
|
|
|
|
pub fn buildCompositeKeysConditions(comptime TableShape: type, comptime keys: []const []const u8, conditionsBuilder: _conditions.Builder, modelKey: anytype) ![keys.len]_sql.RawQuery {
|
|
|
|
// Conditions list for all keys in the composite key.
|
|
|
|
var conditions: [keys.len]_sql.RawQuery = undefined;
|
|
|
|
|
|
|
|
inline for (keys, &conditions) |keyName, *condition| {
|
|
|
|
const keyType = std.meta.fields(TableShape)[std.meta.fieldIndex(TableShape, keyName).?].type;
|
|
|
|
|
|
|
|
if (std.meta.fieldIndex(@TypeOf(modelKey), keyName)) |_| {
|
|
|
|
// The field exists in the key structure, create its condition.
|
|
|
|
condition.* = try conditionsBuilder.value(keyType, keyName, "=", @field(modelKey, keyName));
|
|
|
|
} else {
|
|
|
|
// The field doesn't exist, compilation error.
|
|
|
|
@compileError("The key structure must include a field for " ++ keyName);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return conditions for the current model key.
|
|
|
|
return conditions;
|
|
|
|
}
|