Relations definition and SQL query build improvements.
+ Relations definition. + Add test submodels in testing database. * Generalize raw SQL query preparation and build. * Simplify and improve SQL query build algorithms.
This commit is contained in:
parent
f39fc2cadd
commit
5a2964622c
15 changed files with 1071 additions and 420 deletions
|
@ -10,16 +10,10 @@
|
||||||
.hash = "12200fe147879d72381633e6f44d76db2c8a603cda1969b4e474c15c31052dbb24b7",
|
.hash = "12200fe147879d72381633e6f44d76db2c8a603cda1969b4e474c15c31052dbb24b7",
|
||||||
},
|
},
|
||||||
.pg = .{
|
.pg = .{
|
||||||
.url = "git+https://github.com/karlseguin/pg.zig?ref=zig-0.13#239a4468163a49d8c0d03285632eabe96003e9e2",
|
.url = "git+https://github.com/Madeorsk/pg.zig?ref=expose-mapper-zig-0.13#11c91a714858c517539069fca620918c77a771c7",
|
||||||
.hash = "1220a1d7e51e2fa45e547c76a9e099c09d06e14b0b9bfc6baa89367f56f1ded399a0",
|
.hash = "122047c505cca855c4fb2c831bc6056527175bbd111fe2dc77488799b576da213075",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
.paths = .{
|
.paths = .{ "build.zig", "build.zig.zon", "src", "README.md", "LICENSE" },
|
||||||
"build.zig",
|
|
||||||
"build.zig.zon",
|
|
||||||
"src",
|
|
||||||
"README.md",
|
|
||||||
"LICENSE"
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ const errors = @import("errors.zig");
|
||||||
const Static = @This();
|
const Static = @This();
|
||||||
|
|
||||||
/// Create a value condition on a column.
|
/// Create a value condition on a column.
|
||||||
pub fn value(comptime ValueType: type, allocator: std.mem.Allocator, comptime _column: []const u8, comptime operator: []const u8, _value: ValueType) !_sql.SqlParams {
|
pub fn value(comptime ValueType: type, allocator: std.mem.Allocator, comptime _column: []const u8, comptime operator: []const u8, _value: ValueType) !_sql.RawQuery {
|
||||||
// Initialize the SQL condition string.
|
// Initialize the SQL condition string.
|
||||||
var comptimeSql: [_column.len + 1 + operator.len + 1 + 1]u8 = undefined;
|
var comptimeSql: [_column.len + 1 + operator.len + 1 + 1]u8 = undefined;
|
||||||
@memcpy(comptimeSql[0.._column.len], _column);
|
@memcpy(comptimeSql[0.._column.len], _column);
|
||||||
|
@ -18,8 +18,8 @@ pub fn value(comptime ValueType: type, allocator: std.mem.Allocator, comptime _c
|
||||||
std.mem.copyForwards(u8, sqlBuf, &comptimeSql);
|
std.mem.copyForwards(u8, sqlBuf, &comptimeSql);
|
||||||
|
|
||||||
// Initialize parameters array.
|
// Initialize parameters array.
|
||||||
const params = try allocator.alloc(_sql.QueryParameter, 1);
|
const params = try allocator.alloc(_sql.RawQueryParameter, 1);
|
||||||
params[0] = try _sql.QueryParameter.fromValue(_value);
|
params[0] = try _sql.RawQueryParameter.fromValue(_value);
|
||||||
|
|
||||||
// Return the built SQL condition.
|
// Return the built SQL condition.
|
||||||
return .{
|
return .{
|
||||||
|
@ -29,7 +29,7 @@ pub fn value(comptime ValueType: type, allocator: std.mem.Allocator, comptime _c
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a column condition on a column.
|
/// Create a column condition on a column.
|
||||||
pub fn column(allocator: std.mem.Allocator, comptime _column: []const u8, comptime operator: []const u8, comptime valueColumn: []const u8) !_sql.SqlParams {
|
pub fn column(allocator: std.mem.Allocator, comptime _column: []const u8, comptime operator: []const u8, comptime valueColumn: []const u8) !_sql.RawQuery {
|
||||||
// Initialize the SQL condition string.
|
// Initialize the SQL condition string.
|
||||||
var comptimeSql: [_column.len + 1 + operator.len + 1 + valueColumn.len]u8 = undefined;
|
var comptimeSql: [_column.len + 1 + operator.len + 1 + valueColumn.len]u8 = undefined;
|
||||||
@memcpy(comptimeSql[0.._column.len], _column);
|
@memcpy(comptimeSql[0.._column.len], _column);
|
||||||
|
@ -45,19 +45,19 @@ pub fn column(allocator: std.mem.Allocator, comptime _column: []const u8, compti
|
||||||
// Return the built SQL condition.
|
// Return the built SQL condition.
|
||||||
return .{
|
return .{
|
||||||
.sql = sqlBuf,
|
.sql = sqlBuf,
|
||||||
.params = &[0]_sql.QueryParameter{},
|
.params = &[0]_sql.RawQueryParameter{},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an IN condition on a column.
|
/// Create an IN condition on a column.
|
||||||
pub fn in(comptime ValueType: type, allocator: std.mem.Allocator, _column: []const u8, _value: []const ValueType) !_sql.SqlParams {
|
pub fn in(comptime ValueType: type, allocator: std.mem.Allocator, _column: []const u8, _value: []const ValueType) !_sql.RawQuery {
|
||||||
// Generate parameters SQL.
|
// Generate parameters SQL.
|
||||||
const parametersSql = try _sql.generateParametersSql(allocator, _value.len);
|
const parametersSql = try _sql.generateParametersSql(allocator, _value.len);
|
||||||
// Get all query parameters from given values.
|
// Get all query parameters from given values.
|
||||||
var valueParameters: []_sql.QueryParameter = try allocator.alloc(_sql.QueryParameter, _value.len);
|
var valueParameters: []_sql.RawQueryParameter = try allocator.alloc(_sql.RawQueryParameter, _value.len);
|
||||||
for (0.._value.len) |i| {
|
for (0.._value.len) |i| {
|
||||||
// Convert every given value to a query parameter.
|
// Convert every given value to a query parameter.
|
||||||
valueParameters[i] = try _sql.QueryParameter.fromValue(_value[i]);
|
valueParameters[i] = try _sql.RawQueryParameter.fromValue(_value[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the SQL condition string.
|
// Initialize the SQL condition string.
|
||||||
|
@ -75,7 +75,7 @@ pub fn in(comptime ValueType: type, allocator: std.mem.Allocator, _column: []con
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generic conditions combiner generator.
|
/// Generic conditions combiner generator.
|
||||||
fn conditionsCombiner(comptime keyword: []const u8, allocator: std.mem.Allocator, subconditions: []const _sql.SqlParams) !_sql.SqlParams {
|
fn conditionsCombiner(comptime keyword: []const u8, allocator: std.mem.Allocator, subconditions: []const _sql.RawQuery) !_sql.RawQuery {
|
||||||
if (subconditions.len == 0) {
|
if (subconditions.len == 0) {
|
||||||
// At least one condition is required.
|
// At least one condition is required.
|
||||||
return errors.ZrmError.AtLeastOneConditionRequired;
|
return errors.ZrmError.AtLeastOneConditionRequired;
|
||||||
|
@ -97,7 +97,7 @@ fn conditionsCombiner(comptime keyword: []const u8, allocator: std.mem.Allocator
|
||||||
// Initialize the SQL condition string.
|
// Initialize the SQL condition string.
|
||||||
var sqlBuf = try allocator.alloc(u8, sqlSize);
|
var sqlBuf = try allocator.alloc(u8, sqlSize);
|
||||||
// Initialize the query parameters array.
|
// Initialize the query parameters array.
|
||||||
var parameters = try allocator.alloc(_sql.QueryParameter, queryParametersCount);
|
var parameters = try allocator.alloc(_sql.RawQueryParameter, queryParametersCount);
|
||||||
var sqlBufCursor: usize = 0; var parametersCursor: usize = 0;
|
var sqlBufCursor: usize = 0; var parametersCursor: usize = 0;
|
||||||
|
|
||||||
// Add first parenthesis.
|
// Add first parenthesis.
|
||||||
|
@ -117,7 +117,7 @@ fn conditionsCombiner(comptime keyword: []const u8, allocator: std.mem.Allocator
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add query parameters to the array.
|
// Add query parameters to the array.
|
||||||
std.mem.copyForwards(_sql.QueryParameter, parameters[parametersCursor..parametersCursor+subcondition.params.len], subcondition.params);
|
std.mem.copyForwards(_sql.RawQueryParameter, parameters[parametersCursor..parametersCursor+subcondition.params.len], subcondition.params);
|
||||||
parametersCursor += subcondition.params.len;
|
parametersCursor += subcondition.params.len;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,12 +132,12 @@ fn conditionsCombiner(comptime keyword: []const u8, allocator: std.mem.Allocator
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an AND condition between multiple sub-conditions.
|
/// Create an AND condition between multiple sub-conditions.
|
||||||
pub fn @"and"(allocator: std.mem.Allocator, subconditions: []const _sql.SqlParams) !_sql.SqlParams {
|
pub fn @"and"(allocator: std.mem.Allocator, subconditions: []const _sql.RawQuery) !_sql.RawQuery {
|
||||||
return conditionsCombiner("AND", allocator, subconditions);
|
return conditionsCombiner("AND", allocator, subconditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an OR condition between multiple sub-conditions.
|
/// Create an OR condition between multiple sub-conditions.
|
||||||
pub fn @"or"(allocator: std.mem.Allocator, subconditions: []const _sql.SqlParams) !_sql.SqlParams {
|
pub fn @"or"(allocator: std.mem.Allocator, subconditions: []const _sql.RawQuery) !_sql.RawQuery {
|
||||||
return conditionsCombiner("OR", allocator, subconditions);
|
return conditionsCombiner("OR", allocator, subconditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,27 +148,27 @@ pub const Builder = struct {
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
|
|
||||||
/// Create a value condition on a column.
|
/// Create a value condition on a column.
|
||||||
pub fn value(self: Self, comptime ValueType: type, comptime _column: []const u8, comptime operator: []const u8, _value: ValueType) !_sql.SqlParams {
|
pub fn value(self: Self, comptime ValueType: type, comptime _column: []const u8, comptime operator: []const u8, _value: ValueType) !_sql.RawQuery {
|
||||||
return Static.value(ValueType, self.allocator, _column, operator, _value);
|
return Static.value(ValueType, self.allocator, _column, operator, _value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a column condition on a column.
|
/// Create a column condition on a column.
|
||||||
pub fn column(self: Self, comptime _column: []const u8, comptime operator: []const u8, comptime valueColumn: []const u8) !_sql.SqlParams {
|
pub fn column(self: Self, comptime _column: []const u8, comptime operator: []const u8, comptime valueColumn: []const u8) !_sql.RawQuery {
|
||||||
return Static.column(self.allocator, _column, operator, valueColumn);
|
return Static.column(self.allocator, _column, operator, valueColumn);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an IN condition on a column.
|
/// Create an IN condition on a column.
|
||||||
pub fn in(self: Self, comptime ValueType: type, _column: []const u8, _value: []const ValueType) !_sql.SqlParams {
|
pub fn in(self: Self, comptime ValueType: type, _column: []const u8, _value: []const ValueType) !_sql.RawQuery {
|
||||||
return Static.in(ValueType, self.allocator, _column, _value);
|
return Static.in(ValueType, self.allocator, _column, _value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an AND condition between multiple sub-conditions.
|
/// Create an AND condition between multiple sub-conditions.
|
||||||
pub fn @"and"(self: Self, subconditions: []const _sql.SqlParams) !_sql.SqlParams {
|
pub fn @"and"(self: Self, subconditions: []const _sql.RawQuery) !_sql.RawQuery {
|
||||||
return Static.@"and"(self.allocator, subconditions);
|
return Static.@"and"(self.allocator, subconditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an OR condition between multiple sub-conditions.
|
/// Create an OR condition between multiple sub-conditions.
|
||||||
pub fn @"or"(self: Self, subconditions: []const _sql.SqlParams) !_sql.SqlParams {
|
pub fn @"or"(self: Self, subconditions: []const _sql.RawQuery) !_sql.RawQuery {
|
||||||
return Static.@"or"(self.allocator, subconditions);
|
return Static.@"or"(self.allocator, subconditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ pub fn Insertable(comptime StructType: type) type {
|
||||||
pub fn RepositoryInsertConfiguration(comptime InsertShape: type) type {
|
pub fn RepositoryInsertConfiguration(comptime InsertShape: type) type {
|
||||||
return struct {
|
return struct {
|
||||||
values: []const Insertable(InsertShape) = undefined,
|
values: []const Insertable(InsertShape) = undefined,
|
||||||
returning: ?_sql.SqlParams = null,
|
returning: ?_sql.RawQuery = null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,7 +187,7 @@ pub fn RepositoryInsert(comptime Model: type, comptime TableShape: type, comptim
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set selected columns for RETURNING clause.
|
/// Set selected columns for RETURNING clause.
|
||||||
pub fn returning(self: *Self, _select: _sql.SqlParams) void {
|
pub fn returning(self: *Self, _select: _sql.RawQuery) void {
|
||||||
self.insertConfig.returning = _select;
|
self.insertConfig.returning = _select;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,7 +200,7 @@ pub fn RepositoryInsert(comptime Model: type, comptime TableShape: type, comptim
|
||||||
self.returning(.{
|
self.returning(.{
|
||||||
// Join selected columns.
|
// Join selected columns.
|
||||||
.sql = std.mem.join(self.arena.allocator(), ", ", _select),
|
.sql = std.mem.join(self.arena.allocator(), ", ", _select),
|
||||||
.params = &[_]_sql.QueryParameter{}, // No parameters.
|
.params = &[_]_sql.RawQueryParameter{}, // No parameters.
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,7 +208,7 @@ pub fn RepositoryInsert(comptime Model: type, comptime TableShape: type, comptim
|
||||||
pub fn returningAll(self: *Self) void {
|
pub fn returningAll(self: *Self) void {
|
||||||
self.returning(.{
|
self.returning(.{
|
||||||
.sql = "*",
|
.sql = "*",
|
||||||
.params = &[_]_sql.QueryParameter{}, // No parameters.
|
.params = &[_]_sql.RawQueryParameter{}, // No parameters.
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,56 +236,53 @@ pub fn RepositoryInsert(comptime Model: type, comptime TableShape: type, comptim
|
||||||
) else 0;
|
) else 0;
|
||||||
|
|
||||||
// Initialize SQL buffer.
|
// Initialize SQL buffer.
|
||||||
const sqlBuf = try self.arena.allocator().alloc(u8, fixedSqlSize + valuesSqlSize + returningSize);
|
var sqlBuf = try std.ArrayList(u8).initCapacity(self.arena.allocator(), fixedSqlSize + valuesSqlSize + returningSize);
|
||||||
|
defer sqlBuf.deinit();
|
||||||
|
|
||||||
// Append initial "INSERT INTO table VALUES ".
|
// Append initial "INSERT INTO table VALUES ".
|
||||||
@memcpy(sqlBuf[0..sqlBase.len],sqlBase);
|
try sqlBuf.appendSlice(sqlBase);
|
||||||
var sqlBufCursor: usize = sqlBase.len;
|
|
||||||
|
|
||||||
// Start parameter counter at 1.
|
// Start parameter counter at 1.
|
||||||
var currentParameter: usize = 1;
|
var currentParameter: usize = 1;
|
||||||
|
|
||||||
if (self.insertConfig.values.len == 0) {
|
if (self.insertConfig.values.len == 0) {
|
||||||
// No values, output an empty values set.
|
// No values, output an empty values set.
|
||||||
std.mem.copyForwards(u8, sqlBuf[sqlBufCursor..sqlBufCursor+2], "()");
|
try sqlBuf.appendSlice("()");
|
||||||
sqlBufCursor += 2;
|
|
||||||
} else {
|
} else {
|
||||||
// Build values set.
|
// Build values set.
|
||||||
for (self.insertConfig.values) |_| {
|
for (self.insertConfig.values) |_| {
|
||||||
// Add the first '('.
|
// Add the first '('.
|
||||||
sqlBuf[sqlBufCursor] = '('; sqlBufCursor += 1;
|
try sqlBuf.append('(');
|
||||||
inline for (columns) |_| {
|
inline for (columns) |_| {
|
||||||
// Create the parameter string and append it to the SQL buffer.
|
// Create the parameter string and append it to the SQL buffer.
|
||||||
const paramSize = 1 + try _sql.computeRequiredSpaceForParameter(currentParameter) + 1;
|
try sqlBuf.writer().print("${d},", .{currentParameter});
|
||||||
_ = try std.fmt.bufPrint(sqlBuf[sqlBufCursor..sqlBufCursor+paramSize], "${d},", .{currentParameter});
|
|
||||||
sqlBufCursor += paramSize;
|
|
||||||
// Increment parameter count.
|
// Increment parameter count.
|
||||||
currentParameter += 1;
|
currentParameter += 1;
|
||||||
}
|
}
|
||||||
// Replace the final ',' with a ')'.
|
// Replace the final ',' with a ')'.
|
||||||
sqlBuf[sqlBufCursor - 1] = ')';
|
sqlBuf.items[sqlBuf.items.len - 1] = ')';
|
||||||
// Add the final ','.
|
// Add the final ','.
|
||||||
sqlBuf[sqlBufCursor] = ','; sqlBufCursor += 1;
|
try sqlBuf.append(',');
|
||||||
}
|
}
|
||||||
sqlBufCursor -= 1;
|
|
||||||
|
// Remove the last ','.
|
||||||
|
_ = sqlBuf.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append RETURNING clause, if there is one defined.
|
// Append RETURNING clause, if there is one defined.
|
||||||
if (self.insertConfig.returning) |_returning| {
|
if (self.insertConfig.returning) |_returning| {
|
||||||
@memcpy(sqlBuf[sqlBufCursor..sqlBufCursor+(1 + returningClause.len + 1)], " " ++ returningClause ++ " ");
|
try sqlBuf.appendSlice(" " ++ returningClause ++ " ");
|
||||||
// Copy RETURNING clause content and replace parameters, if there are some.
|
// Copy RETURNING clause content and replace parameters, if there are some.
|
||||||
try _sql.copyAndReplaceSqlParameters(¤tParameter,
|
try _sql.copyAndReplaceSqlParameters(¤tParameter,
|
||||||
_returning.params.len,
|
_returning.params.len, sqlBuf.writer(), _returning.sql
|
||||||
sqlBuf[sqlBufCursor+(1+returningClause.len+1)..sqlBufCursor+returningSize], _returning.sql
|
|
||||||
);
|
);
|
||||||
sqlBufCursor += returningSize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ";" to end the query.
|
// ";" to end the query.
|
||||||
sqlBuf[sqlBufCursor] = ';'; sqlBufCursor += 1;
|
try sqlBuf.append(';');
|
||||||
|
|
||||||
// Save built SQL query.
|
// Save built SQL query.
|
||||||
self.sql = sqlBuf;
|
self.sql = try sqlBuf.toOwnedSlice();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute the insert query.
|
/// Execute the insert query.
|
||||||
|
|
|
@ -5,6 +5,7 @@ const global = @import("global.zig");
|
||||||
const errors = @import("errors.zig");
|
const errors = @import("errors.zig");
|
||||||
const database = @import("database.zig");
|
const database = @import("database.zig");
|
||||||
const _sql = @import("sql.zig");
|
const _sql = @import("sql.zig");
|
||||||
|
const _relations = @import("relations.zig");
|
||||||
const repository = @import("repository.zig");
|
const repository = @import("repository.zig");
|
||||||
|
|
||||||
/// PostgreSQL query error details.
|
/// PostgreSQL query error details.
|
||||||
|
@ -14,7 +15,7 @@ pub const PostgresqlError = struct {
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Try to bind query parameters to the statement.
|
/// Try to bind query parameters to the statement.
|
||||||
pub fn bindQueryParameters(statement: *pg.Stmt, parameters: []const _sql.QueryParameter) !void {
|
pub fn bindQueryParameters(statement: *pg.Stmt, parameters: []const _sql.RawQueryParameter) !void {
|
||||||
for (parameters) |parameter| {
|
for (parameters) |parameter| {
|
||||||
// Try to bind each parameter in the slice.
|
// Try to bind each parameter in the slice.
|
||||||
try bindQueryParameter(statement, parameter);
|
try bindQueryParameter(statement, parameter);
|
||||||
|
@ -22,7 +23,7 @@ pub fn bindQueryParameters(statement: *pg.Stmt, parameters: []const _sql.QueryPa
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to bind a query parameter to the statement.
|
/// Try to bind a query parameter to the statement.
|
||||||
pub fn bindQueryParameter(statement: *pg.Stmt, parameter: _sql.QueryParameter) !void {
|
pub fn bindQueryParameter(statement: *pg.Stmt, parameter: _sql.RawQueryParameter) !void {
|
||||||
switch (parameter) {
|
switch (parameter) {
|
||||||
.integer => |integer| try statement.bind(integer),
|
.integer => |integer| try statement.bind(integer),
|
||||||
.number => |number| try statement.bind(number),
|
.number => |number| try statement.bind(number),
|
||||||
|
@ -57,6 +58,27 @@ pub fn handleRawPostgresqlError(err: anyerror, connection: *pg.Conn) anyerror {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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) {
|
||||||
|
var column_indexes: [std.meta.fields(T).len]?usize = undefined;
|
||||||
|
|
||||||
|
inline for (std.meta.fields(T), 0..) |field, i| {
|
||||||
|
if (optionalPrefix) |prefix| {
|
||||||
|
const fullName = try std.fmt.allocPrint(allocator, "{s}" ++ field.name, .{prefix});
|
||||||
|
defer allocator.free(fullName);
|
||||||
|
column_indexes[i] = result.columnIndex(fullName);
|
||||||
|
} else {
|
||||||
|
column_indexes[i] = result.columnIndex(field.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.result = result,
|
||||||
|
.allocator = allocator,
|
||||||
|
.column_indexes = column_indexes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Generic query results mapping.
|
/// Generic query results mapping.
|
||||||
pub fn mapResults(comptime Model: type, comptime TableShape: type,
|
pub fn mapResults(comptime Model: type, comptime TableShape: type,
|
||||||
repositoryConfig: repository.RepositoryConfiguration(Model, TableShape),
|
repositoryConfig: repository.RepositoryConfiguration(Model, TableShape),
|
||||||
|
@ -66,7 +88,7 @@ pub fn mapResults(comptime Model: type, comptime TableShape: type,
|
||||||
// Create an arena for mapper data.
|
// Create an arena for mapper data.
|
||||||
var mapperArena = std.heap.ArenaAllocator.init(allocator);
|
var mapperArena = std.heap.ArenaAllocator.init(allocator);
|
||||||
// Get result mapper.
|
// Get result mapper.
|
||||||
const mapper = queryResult.mapper(TableShape, .{ .allocator = mapperArena.allocator() });
|
const mapper = try makeMapper(TableShape, queryResult, mapperArena.allocator(), null);
|
||||||
|
|
||||||
// Initialize models list.
|
// Initialize models list.
|
||||||
var models = std.ArrayList(*Model).init(allocator);
|
var models = std.ArrayList(*Model).init(allocator);
|
||||||
|
|
341
src/query.zig
341
src/query.zig
|
@ -5,26 +5,31 @@ const errors = @import("errors.zig");
|
||||||
const database = @import("database.zig");
|
const database = @import("database.zig");
|
||||||
const postgresql = @import("postgresql.zig");
|
const postgresql = @import("postgresql.zig");
|
||||||
const _sql = @import("sql.zig");
|
const _sql = @import("sql.zig");
|
||||||
const conditions = @import("conditions.zig");
|
const _conditions = @import("conditions.zig");
|
||||||
|
const relations = @import("relations.zig");
|
||||||
const repository = @import("repository.zig");
|
const repository = @import("repository.zig");
|
||||||
|
|
||||||
|
const InlineRelationsResult = struct {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
/// Repository query configuration structure.
|
/// Repository query configuration structure.
|
||||||
pub const RepositoryQueryConfiguration = struct {
|
pub const RepositoryQueryConfiguration = struct {
|
||||||
select: ?_sql.SqlParams = null,
|
select: ?_sql.RawQuery = null,
|
||||||
join: ?_sql.SqlParams = null,
|
join: ?_sql.RawQuery = null,
|
||||||
where: ?_sql.SqlParams = null,
|
where: ?_sql.RawQuery = null,
|
||||||
|
with: ?[]const relations.Eager = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Repository models query manager.
|
/// Repository models query manager.
|
||||||
/// Manage query string build and its execution.
|
/// Manage query string build and its execution.
|
||||||
pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime repositoryConfig: repository.RepositoryConfiguration(Model, TableShape)) type {
|
pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime repositoryConfig: repository.RepositoryConfiguration(Model, TableShape)) type {
|
||||||
// Pre-compute SQL buffer size.
|
// Pre-compute SQL buffer size.
|
||||||
const selectClause = "SELECT";
|
const fromClause = " FROM \"" ++ repositoryConfig.table ++ "\"";
|
||||||
const fromClause = "FROM";
|
const defaultSelectSql = "\"" ++ repositoryConfig.table ++ "\".*";
|
||||||
const whereClause = "WHERE";
|
|
||||||
// SELECT ? FROM {repositoryConfig.table}??;
|
// Model key type.
|
||||||
const fixedSqlSize = selectClause.len + 1 + 0 + 1 + fromClause.len + 1 + repositoryConfig.table.len + 0 + 0 + 1;
|
const KeyType = repository.ModelKeyType(Model, TableShape, repositoryConfig);
|
||||||
const defaultSelectSql = "*";
|
|
||||||
|
|
||||||
return struct {
|
return struct {
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
@ -34,10 +39,14 @@ pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime
|
||||||
connection: *database.Connection = undefined,
|
connection: *database.Connection = undefined,
|
||||||
queryConfig: RepositoryQueryConfiguration,
|
queryConfig: RepositoryQueryConfiguration,
|
||||||
|
|
||||||
|
/// List of loaded inline relations.
|
||||||
|
inlineRelations: []relations.Eager = undefined,
|
||||||
|
|
||||||
|
query: ?_sql.RawQuery = null,
|
||||||
sql: ?[]const u8 = null,
|
sql: ?[]const u8 = null,
|
||||||
|
|
||||||
/// Set selected columns.
|
/// Set selected columns.
|
||||||
pub fn select(self: *Self, _select: _sql.SqlParams) void {
|
pub fn select(self: *Self, _select: _sql.RawQuery) void {
|
||||||
self.queryConfig.select = _select;
|
self.queryConfig.select = _select;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,131 +59,236 @@ pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime
|
||||||
self.select(.{
|
self.select(.{
|
||||||
// Join selected columns.
|
// Join selected columns.
|
||||||
.sql = std.mem.join(self.arena.allocator(), ", ", _select),
|
.sql = std.mem.join(self.arena.allocator(), ", ", _select),
|
||||||
.params = &[_]_sql.QueryParameter{}, // No parameters.
|
.params = &[_]_sql.RawQueryParameter{}, // No parameters.
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set JOIN clause.
|
/// Set JOIN clause.
|
||||||
pub fn join(self: *Self, _join: _sql.SqlParams) void {
|
pub fn join(self: *Self, _join: _sql.RawQuery) void {
|
||||||
self.queryConfig.join = _join;
|
self.queryConfig.join = _join;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set WHERE conditions.
|
/// Set WHERE conditions.
|
||||||
pub fn where(self: *Self, _where: _sql.SqlParams) void {
|
pub fn where(self: *Self, _where: _sql.RawQuery) void {
|
||||||
self.queryConfig.where = _where;
|
self.queryConfig.where = _where;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new condition builder.
|
/// Create a new condition builder.
|
||||||
pub fn newCondition(self: *Self) conditions.Builder {
|
pub fn newCondition(self: *Self) _conditions.Builder {
|
||||||
return conditions.Builder.init(self.arena.allocator());
|
return _conditions.Builder.init(self.arena.allocator());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a WHERE value condition.
|
/// Set a WHERE value condition.
|
||||||
pub fn whereValue(self: *Self, comptime ValueType: type, comptime _column: []const u8, comptime operator: []const u8, _value: ValueType) !void {
|
pub fn whereValue(self: *Self, comptime ValueType: type, comptime _column: []const u8, comptime operator: []const u8, _value: ValueType) !void {
|
||||||
self.where(
|
self.where(
|
||||||
try conditions.value(ValueType, self.arena.allocator(), _column, operator, _value)
|
try _conditions.value(ValueType, self.arena.allocator(), _column, operator, _value)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a WHERE column condition.
|
/// Set a WHERE column condition.
|
||||||
pub fn whereColumn(self: *Self, comptime _column: []const u8, comptime operator: []const u8, comptime _valueColumn: []const u8) !void {
|
pub fn whereColumn(self: *Self, comptime _column: []const u8, comptime operator: []const u8, comptime _valueColumn: []const u8) !void {
|
||||||
self.where(
|
self.where(
|
||||||
try conditions.column(self.arena.allocator(), _column, operator, _valueColumn)
|
try _conditions.column(self.arena.allocator(), _column, operator, _valueColumn)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a WHERE IN condition.
|
/// Set a WHERE IN condition.
|
||||||
pub fn whereIn(self: *Self, comptime ValueType: type, comptime _column: []const u8, _value: []const ValueType) !void {
|
pub fn whereIn(self: *Self, comptime ValueType: type, comptime _column: []const u8, _value: []const ValueType) !void {
|
||||||
self.where(
|
self.where(
|
||||||
try conditions.in(ValueType, self.arena.allocator(), _column, _value)
|
try _conditions.in(ValueType, self.arena.allocator(), _column, _value)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set relations to eager load.
|
||||||
|
pub fn with(self: *Self, relation: relations.ModelRelation) !void {
|
||||||
|
// Take an array of eager relations (which can have subrelations).
|
||||||
|
const allocator = self.arena.allocator();
|
||||||
|
|
||||||
|
// Make a relation instance.
|
||||||
|
const relationInstance = try allocator.create(relation.relation);
|
||||||
|
|
||||||
|
// Add the new relation to a newly allocated array, with one more space.
|
||||||
|
const newPos = if (self.queryConfig.with) |_with| _with.len else 0;
|
||||||
|
var newWith = try allocator.alloc(relations.Eager, newPos + 1);
|
||||||
|
newWith[newPos] = .{
|
||||||
|
.field = relation.field,
|
||||||
|
.relation = relationInstance.*.relation(),
|
||||||
|
.with = &[0]relations.Eager{}, //TODO handle subrelations with dotted syntax
|
||||||
|
};
|
||||||
|
|
||||||
|
if (self.queryConfig.with) |_with| {
|
||||||
|
// Copy existing relations.
|
||||||
|
@memcpy(newWith[0..newPos], _with);
|
||||||
|
// Free previous array.
|
||||||
|
allocator.free(_with);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the newly allocated array.
|
||||||
|
self.queryConfig.with = newWith;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build inline relations query part.
|
||||||
|
fn buildInlineRelations(self: *Self) !?struct{
|
||||||
|
select: []const u8,
|
||||||
|
join: _sql.RawQuery,
|
||||||
|
} {
|
||||||
|
if (self.queryConfig.with) |_with| {
|
||||||
|
// Initialize an ArrayList of query parts for relations.
|
||||||
|
var inlineRelations = try std.ArrayList(_sql.RawQuery).initCapacity(self.arena.allocator(), _with.len);
|
||||||
|
defer inlineRelations.deinit();
|
||||||
|
var inlineRelationsSelect = try std.ArrayList([]const u8).initCapacity(self.arena.allocator(), _with.len);
|
||||||
|
defer inlineRelationsSelect.deinit();
|
||||||
|
|
||||||
|
// Initialize an ArrayList to store all loaded inline relations.
|
||||||
|
var loadedRelations = std.ArrayList(relations.Eager).init(self.arena.allocator());
|
||||||
|
defer loadedRelations.deinit();
|
||||||
|
|
||||||
|
for (_with) |_relation| {
|
||||||
|
// Append each inline relation to the ArrayList.
|
||||||
|
if (_relation.relation.inlineMapping()) {
|
||||||
|
try loadedRelations.append(_relation); // Store the loaded inline relation.
|
||||||
|
|
||||||
|
// Get an allocator for local allocations.
|
||||||
|
const localAllocator = self.arena.allocator();
|
||||||
|
|
||||||
|
// Build table alias and fields prefix.
|
||||||
|
const tableAlias = try std.fmt.allocPrint(localAllocator, "relations.{s}", .{_relation.field});
|
||||||
|
defer localAllocator.free(tableAlias);
|
||||||
|
const prefix = try std.fmt.allocPrint(localAllocator, "{s}.", .{tableAlias});
|
||||||
|
defer localAllocator.free(prefix);
|
||||||
|
|
||||||
|
// Alter query to get relation fields.
|
||||||
|
try inlineRelations.append(try _relation.relation.genJoin(self.arena.allocator(), tableAlias));
|
||||||
|
const relationSelect = try _relation.relation.genSelect(localAllocator, tableAlias, prefix);
|
||||||
|
try inlineRelationsSelect.append(relationSelect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.inlineRelations = try loadedRelations.toOwnedSlice();
|
||||||
|
|
||||||
|
// Return the inline relations query part.
|
||||||
|
return .{
|
||||||
|
.select = try std.mem.join(self.arena.allocator(), ", ", inlineRelationsSelect.items),
|
||||||
|
.join = try _sql.RawQuery.fromConcat(self.arena.allocator(), inlineRelations.items),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Nothing.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Build SQL query.
|
/// Build SQL query.
|
||||||
pub fn buildSql(self: *Self) !void {
|
pub fn buildSql(self: *Self) !void {
|
||||||
// Start parameter counter at 1.
|
// Build inline relations query part.
|
||||||
var currentParameter: usize = 1;
|
const inlineRelations = try self.buildInlineRelations();
|
||||||
|
defer if (inlineRelations) |_inlineRelations| self.arena.allocator().free(_inlineRelations.join.sql);
|
||||||
|
defer if (inlineRelations) |_inlineRelations| self.arena.allocator().free(_inlineRelations.join.params);
|
||||||
|
defer if (inlineRelations) |_inlineRelations| self.arena.allocator().free(_inlineRelations.select);
|
||||||
|
|
||||||
// Compute SELECT size.
|
// Build the full SQL query from all its parts.
|
||||||
var selectSize: usize = defaultSelectSql.len;
|
const sqlQuery = _sql.RawQuery{
|
||||||
if (self.queryConfig.select) |_select| {
|
.sql = try std.mem.join(self.arena.allocator(), "", &[_][]const u8{
|
||||||
selectSize = _select.sql.len + _sql.computeRequiredSpaceForParametersNumbers(_select.params.len, currentParameter - 1);
|
"SELECT ", if (self.queryConfig.select) |_select| _select.sql else defaultSelectSql,
|
||||||
currentParameter += _select.params.len;
|
if (inlineRelations) |_| ", " else "",
|
||||||
}
|
if (inlineRelations) |_inlineRelations| _inlineRelations.select else "",
|
||||||
|
fromClause,
|
||||||
// Compute JOIN size.
|
if (self.queryConfig.join) |_| " " else "",
|
||||||
var joinSize: usize = 0;
|
if (self.queryConfig.join) |_join| _join.sql else "",
|
||||||
if (self.queryConfig.join) |_join| {
|
if (inlineRelations) |_| " " else "",
|
||||||
joinSize = 1 + _join.sql.len + _sql.computeRequiredSpaceForParametersNumbers(_join.params.len, currentParameter - 1);
|
if (inlineRelations) |_inlineRelations| _inlineRelations.join.sql else "",
|
||||||
currentParameter += _join.params.len;
|
if (self.queryConfig.where) |_| " WHERE " else "",
|
||||||
}
|
if (self.queryConfig.where) |_where| _where.sql else "",
|
||||||
|
";",
|
||||||
// Compute WHERE size.
|
}),
|
||||||
var whereSize: usize = 0;
|
.params = try std.mem.concat(self.arena.allocator(), _sql.RawQueryParameter, &[_][]const _sql.RawQueryParameter{
|
||||||
if (self.queryConfig.where) |_where| {
|
if (self.queryConfig.select) |_select| _select.params else &[0]_sql.RawQueryParameter{},
|
||||||
whereSize = 1 + whereClause.len + _where.sql.len + 1 + _sql.computeRequiredSpaceForParametersNumbers(_where.params.len, currentParameter - 1);
|
if (self.queryConfig.join) |_join| _join.params else &[0]_sql.RawQueryParameter{},
|
||||||
currentParameter += _where.params.len;
|
if (inlineRelations) |_inlineRelations| _inlineRelations.join.params else &[0]_sql.RawQueryParameter{},
|
||||||
}
|
if (self.queryConfig.where) |_where| _where.params else &[0]_sql.RawQueryParameter{},
|
||||||
|
})
|
||||||
// Allocate SQL buffer from computed size.
|
};
|
||||||
const sqlBuf = try self.arena.allocator().alloc(u8, fixedSqlSize
|
|
||||||
+ (selectSize)
|
|
||||||
+ (joinSize)
|
|
||||||
+ (whereSize)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fill SQL buffer.
|
|
||||||
|
|
||||||
// Restart parameter counter at 1.
|
|
||||||
currentParameter = 1;
|
|
||||||
|
|
||||||
// SELECT clause.
|
|
||||||
@memcpy(sqlBuf[0..selectClause.len+1], selectClause ++ " ");
|
|
||||||
var sqlBufCursor: usize = selectClause.len+1;
|
|
||||||
|
|
||||||
// Copy SELECT clause content and replace parameters, if there are some.
|
|
||||||
try _sql.copyAndReplaceSqlParameters(¤tParameter,
|
|
||||||
if (self.queryConfig.select) |_select| _select.params.len else 0,
|
|
||||||
sqlBuf[sqlBufCursor..sqlBufCursor+selectSize],
|
|
||||||
if (self.queryConfig.select) |_select| _select.sql else defaultSelectSql,
|
|
||||||
);
|
|
||||||
sqlBufCursor += selectSize;
|
|
||||||
|
|
||||||
// FROM clause.
|
|
||||||
sqlBuf[sqlBufCursor] = ' '; sqlBufCursor += 1;
|
|
||||||
std.mem.copyForwards(u8, sqlBuf[sqlBufCursor..sqlBufCursor+fromClause.len], fromClause); sqlBufCursor += fromClause.len;
|
|
||||||
sqlBuf[sqlBufCursor] = ' '; sqlBufCursor += 1;
|
|
||||||
|
|
||||||
// Table name.
|
|
||||||
std.mem.copyForwards(u8, sqlBuf[sqlBufCursor..sqlBufCursor+repositoryConfig.table.len], repositoryConfig.table); sqlBufCursor += repositoryConfig.table.len;
|
|
||||||
|
|
||||||
// JOIN clause.
|
|
||||||
if (self.queryConfig.join) |_join| {
|
|
||||||
sqlBuf[sqlBufCursor] = ' ';
|
|
||||||
// Copy JOIN clause and replace parameters, if there are some.
|
|
||||||
try _sql.copyAndReplaceSqlParameters(¤tParameter,
|
|
||||||
_join.params.len,
|
|
||||||
sqlBuf[sqlBufCursor+1..sqlBufCursor+joinSize], _join.sql
|
|
||||||
);
|
|
||||||
sqlBufCursor += joinSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
// WHERE clause.
|
|
||||||
if (self.queryConfig.where) |_where| {
|
|
||||||
@memcpy(sqlBuf[sqlBufCursor..sqlBufCursor+(1 + whereClause.len + 1)], " " ++ whereClause ++ " ");
|
|
||||||
// Copy WHERE clause content and replace parameters, if there are some.
|
|
||||||
try _sql.copyAndReplaceSqlParameters(¤tParameter,
|
|
||||||
_where.params.len,
|
|
||||||
sqlBuf[sqlBufCursor+(1+whereClause.len+1)..sqlBufCursor+whereSize], _where.sql
|
|
||||||
);
|
|
||||||
sqlBufCursor += whereSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ";" to end the query.
|
|
||||||
sqlBuf[sqlBufCursor] = ';'; sqlBufCursor += 1;
|
|
||||||
|
|
||||||
// Save built SQL query.
|
// Save built SQL query.
|
||||||
self.sql = sqlBuf;
|
self.query = sqlQuery;
|
||||||
|
self.sql = try sqlQuery.build(self.arena.allocator());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute the built query.
|
/// Execute the built query.
|
||||||
|
@ -195,12 +309,8 @@ pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime
|
||||||
catch |err| return postgresql.handlePostgresqlError(err, self.connection, &statement);
|
catch |err| return postgresql.handlePostgresqlError(err, self.connection, &statement);
|
||||||
|
|
||||||
// Bind query parameters.
|
// Bind query parameters.
|
||||||
if (self.queryConfig.select) |_select|
|
postgresql.bindQueryParameters(&statement, self.query.?.params)
|
||||||
try postgresql.bindQueryParameters(&statement, _select.params);
|
catch |err| return postgresql.handlePostgresqlError(err, self.connection, &statement);
|
||||||
if (self.queryConfig.join) |_join|
|
|
||||||
try postgresql.bindQueryParameters(&statement, _join.params);
|
|
||||||
if (self.queryConfig.where) |_where|
|
|
||||||
try postgresql.bindQueryParameters(&statement, _where.params);
|
|
||||||
|
|
||||||
// Execute the query and get its result.
|
// Execute the query and get its result.
|
||||||
const result = statement.execute()
|
const result = statement.execute()
|
||||||
|
@ -241,3 +351,24 @@ pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
}
|
||||||
|
|
343
src/relations.zig
Normal file
343
src/relations.zig
Normal file
|
@ -0,0 +1,343 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const pg = @import("pg");
|
||||||
|
const _sql = @import("sql.zig");
|
||||||
|
const repository = @import("repository.zig");
|
||||||
|
const _query = @import("query.zig");
|
||||||
|
|
||||||
|
/// Configure a "one to many" or "many to many" relation.
|
||||||
|
pub const ManyConfiguration = union(enum) {
|
||||||
|
/// Direct one-to-many relation 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 relation 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" relation.
|
||||||
|
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" relation.
|
||||||
|
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 {
|
||||||
|
|
||||||
|
// Get foreign key from relation 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 relation 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],
|
||||||
|
};
|
||||||
|
|
||||||
|
const FromKeyType = std.meta.fields(FromModel)[std.meta.fieldIndex(FromModel, fromRepositoryConfig.key[0]).?].type;
|
||||||
|
const QueryType = _query.RepositoryQuery(ToModel, ToTable, toRepositoryConfig);
|
||||||
|
const SelectBuilder = _sql.SelectBuilder(ToTable);
|
||||||
|
|
||||||
|
return struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
fn inlineMapping(_: *anyopaque) bool {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn genJoin(_: *anyopaque, _: std.mem.Allocator, _: []const u8) !_sql.RawQuery {
|
||||||
|
unreachable; // No possible join in a many relation.
|
||||||
|
}
|
||||||
|
|
||||||
|
fn genSelect(_: *anyopaque, allocator: std.mem.Allocator, table: []const u8, prefix: []const u8) ![]const u8 {
|
||||||
|
return SelectBuilder.build(allocator, table, prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buildQuery(_: *anyopaque, opaqueModels: []const anyopaque, opaqueQuery: *anyopaque) !void {
|
||||||
|
var models: []const FromModel = undefined;
|
||||||
|
models.len = opaqueModels.len;
|
||||||
|
models.ptr = @ptrCast(@alignCast(opaqueModels.ptr));
|
||||||
|
const query: *QueryType = @ptrCast(@alignCast(opaqueQuery));
|
||||||
|
|
||||||
|
// 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 => {
|
||||||
|
// Build WHERE condition.
|
||||||
|
try query.whereIn(FromKeyType, "\"" ++ toRepositoryConfig.table ++ "\".\"" ++ foreignKey ++ "\"", modelsIds);
|
||||||
|
},
|
||||||
|
.through => |through| {
|
||||||
|
query.join(.{
|
||||||
|
.sql = "INNER JOIN \"" ++ through.table ++ "\" ON " ++
|
||||||
|
"\"" ++ toRepositoryConfig.table ++ "\"." ++ modelKey ++ " = " ++ "\"" ++ through.table ++ "\"." ++ through.joinModelKey,
|
||||||
|
.params = &[0]_sql.RawQueryParameter{},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build WHERE condition.
|
||||||
|
try query.whereIn(FromKeyType, "\"" ++ through.table ++ "\".\"" ++ through.joinForeignKey ++ "\"", modelsIds);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn relation(self: *Self) Relation {
|
||||||
|
return .{
|
||||||
|
._interface = .{
|
||||||
|
.instance = self,
|
||||||
|
|
||||||
|
.inlineMapping = inlineMapping,
|
||||||
|
.genJoin = genJoin,
|
||||||
|
.genSelect = genSelect,
|
||||||
|
.buildQuery = buildQuery,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Configure a "one to one" relation.
|
||||||
|
pub const OneConfiguration = union(enum) {
|
||||||
|
/// Direct one-to-one relation 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 relation using distant foreign key.
|
||||||
|
reverse: struct {
|
||||||
|
/// The distant foreign key name.
|
||||||
|
foreignKey: []const u8,
|
||||||
|
/// Current model key name.
|
||||||
|
/// Use the default key name of the current model.
|
||||||
|
modelKey: ?[]const u8 = null,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Used when performing a one-to-one relation 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" relation.
|
||||||
|
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" relation.
|
||||||
|
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 {
|
||||||
|
|
||||||
|
const FromKeyType = std.meta.fields(FromModel)[std.meta.fieldIndex(FromModel, fromRepositoryConfig.key[0]).?].type;
|
||||||
|
const QueryType = _query.RepositoryQuery(ToModel, ToTable, toRepositoryConfig);
|
||||||
|
const SelectBuilder = _sql.SelectBuilder(ToTable);
|
||||||
|
|
||||||
|
// Get foreign key from relation config or repository config.
|
||||||
|
const foreignKey = switch (config) {
|
||||||
|
.direct => |direct| direct.foreignKey,
|
||||||
|
.reverse => |reverse| reverse.foreignKey,
|
||||||
|
.through => |through| if (through.foreignKey) |_foreignKey| _foreignKey else fromRepositoryConfig.key[0],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get model key from relation 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 toRepositoryConfig.key[0],
|
||||||
|
.through => |through| if (through.modelKey) |_modelKey| _modelKey else toRepositoryConfig.key[0],
|
||||||
|
};
|
||||||
|
|
||||||
|
return struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
fn inlineMapping(_: *anyopaque) bool {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn genJoin(_: *anyopaque, allocator: std.mem.Allocator, alias: []const u8) !_sql.RawQuery {
|
||||||
|
return switch (config) {
|
||||||
|
.direct => (.{
|
||||||
|
.sql = try std.fmt.allocPrint(allocator, "LEFT JOIN \"" ++ toRepositoryConfig.table ++ "\" AS \"{s}\" ON " ++
|
||||||
|
"\"" ++ fromRepositoryConfig.table ++ "\"." ++ foreignKey ++ " = \"{s}\"." ++ modelKey, .{alias, alias}),
|
||||||
|
.params = &[0]_sql.RawQueryParameter{},
|
||||||
|
}),
|
||||||
|
|
||||||
|
.reverse => (.{
|
||||||
|
.sql = try std.fmt.allocPrint(allocator, "LEFT JOIN \"" ++ toRepositoryConfig.table ++ "\" AS \"{s}\" ON " ++
|
||||||
|
"\"" ++ fromRepositoryConfig.table ++ "\"." ++ modelKey ++ " = \"{s}\"." ++ foreignKey, .{alias, alias}),
|
||||||
|
.params = &[0]_sql.RawQueryParameter{},
|
||||||
|
}),
|
||||||
|
|
||||||
|
.through => |through| (.{
|
||||||
|
.sql = try std.fmt.allocPrint(allocator, "LEFT JOIN \"" ++ through.table ++ "\" AS \"{s}_pivot\" ON " ++
|
||||||
|
"\"" ++ fromRepositoryConfig.table ++ "\"." ++ foreignKey ++ " = " ++ "\"{s}_pivot\"." ++ through.joinForeignKey ++
|
||||||
|
"LEFT JOIN \"" ++ toRepositoryConfig.table ++ "\" AS \"{s}\" ON " ++
|
||||||
|
"\"{s}_pivot\"." ++ through.joinModelKey ++ " = " ++ "\"{s}\"." ++ modelKey, .{alias, alias, alias, alias, alias}),
|
||||||
|
.params = &[0]_sql.RawQueryParameter{},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn genSelect(_: *anyopaque, allocator: std.mem.Allocator, table: []const u8, prefix: []const u8) ![]const u8 {
|
||||||
|
return SelectBuilder.build(allocator, table, prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buildQuery(_: *anyopaque, opaqueModels: []const anyopaque, opaqueQuery: *anyopaque) !void {
|
||||||
|
var models: []const FromModel = undefined;
|
||||||
|
models.len = opaqueModels.len;
|
||||||
|
models.ptr = @ptrCast(@alignCast(opaqueModels.ptr));
|
||||||
|
const query: *QueryType = @ptrCast(@alignCast(opaqueQuery));
|
||||||
|
|
||||||
|
// 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 => {
|
||||||
|
query.join((_sql.RawQuery{
|
||||||
|
.sql = "INNER JOIN \"" ++ fromRepositoryConfig.table ++ "\" ON \"" ++ toRepositoryConfig.table ++ "\"." ++ modelKey ++ " = \"" ++ fromRepositoryConfig.table ++ "\"." ++ foreignKey,
|
||||||
|
.params = &[0]_sql.RawQueryParameter{},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Build WHERE condition.
|
||||||
|
try query.whereIn(FromKeyType, "\"" ++ fromRepositoryConfig.table ++ "\".\"" ++ fromRepositoryConfig.key[0] ++ "\"", modelsIds);
|
||||||
|
},
|
||||||
|
.reverse => {
|
||||||
|
// Build WHERE condition.
|
||||||
|
try query.whereIn(FromKeyType, "\"" ++ toRepositoryConfig.table ++ "\".\"" ++ foreignKey ++ "\"", modelsIds);
|
||||||
|
},
|
||||||
|
.through => |through| {
|
||||||
|
query.join(.{
|
||||||
|
.sql = "INNER JOIN \"" ++ through.table ++ "\" ON " ++
|
||||||
|
"\"" ++ toRepositoryConfig.table ++ "\"." ++ modelKey ++ " = " ++ "\"" ++ through.table ++ "\"." ++ through.joinModelKey,
|
||||||
|
.params = &[0]_sql.RawQueryParameter{},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build WHERE condition.
|
||||||
|
try query.whereIn(FromKeyType, "\"" ++ through.table ++ "\".\"" ++ through.joinForeignKey ++ "\"", modelsIds);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn relation(self: *Self) Relation {
|
||||||
|
return .{
|
||||||
|
._interface = .{
|
||||||
|
.instance = self,
|
||||||
|
|
||||||
|
.inlineMapping = inlineMapping,
|
||||||
|
.genJoin = genJoin,
|
||||||
|
.genSelect = genSelect,
|
||||||
|
.buildQuery = buildQuery,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Generic model relation interface.
|
||||||
|
pub const Relation = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
_interface: struct {
|
||||||
|
instance: *anyopaque,
|
||||||
|
|
||||||
|
inlineMapping: *const fn (self: *anyopaque) bool,
|
||||||
|
genJoin: *const fn (self: *anyopaque, allocator: std.mem.Allocator, alias: []const u8) anyerror!_sql.RawQuery,
|
||||||
|
genSelect: *const fn (self: *anyopaque, allocator: std.mem.Allocator, table: []const u8, prefix: []const u8) anyerror![]const u8,
|
||||||
|
buildQuery: *const fn (self: *anyopaque, models: []const anyopaque, query: *anyopaque) anyerror!void,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// In case of inline mapping, generate a JOIN clause to retrieve the associated data.
|
||||||
|
pub fn genJoin(self: Self, allocator: std.mem.Allocator, alias: []const u8) !_sql.RawQuery {
|
||||||
|
return self._interface.genJoin(self._interface.instance, allocator, alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a SELECT clause to retrieve the associated data, with the given table and prefix.
|
||||||
|
pub fn genSelect(self: Self, allocator: std.mem.Allocator, table: []const u8, prefix: []const u8) ![]const u8 {
|
||||||
|
return self._interface.genSelect(self._interface.instance, allocator, 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.
|
||||||
|
pub const ModelRelation = struct {
|
||||||
|
relation: type,
|
||||||
|
field: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/// Structure of an eager loaded relation.
|
||||||
|
pub const Eager = struct {
|
||||||
|
/// Model field to fill for the relation.
|
||||||
|
field: []const u8,
|
||||||
|
/// The relation to eager load.
|
||||||
|
relation: Relation,
|
||||||
|
/// Subrelations to eager load.
|
||||||
|
with: []const Eager,
|
||||||
|
};
|
|
@ -3,6 +3,7 @@ const zollections = @import("zollections");
|
||||||
const database = @import("database.zig");
|
const database = @import("database.zig");
|
||||||
const _sql = @import("sql.zig");
|
const _sql = @import("sql.zig");
|
||||||
const _conditions = @import("conditions.zig");
|
const _conditions = @import("conditions.zig");
|
||||||
|
const _relations = @import("relations.zig");
|
||||||
const query = @import("query.zig");
|
const query = @import("query.zig");
|
||||||
const insert = @import("insert.zig");
|
const insert = @import("insert.zig");
|
||||||
const update = @import("update.zig");
|
const update = @import("update.zig");
|
||||||
|
@ -73,17 +74,84 @@ pub fn ModelKeyType(comptime Model: type, comptime TableShape: type, comptime co
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Model relations definition type.
|
||||||
|
pub fn RelationsDefinitionType(comptime rawDefinition: anytype) type {
|
||||||
|
const rawDefinitionType = @typeInfo(@TypeOf(rawDefinition));
|
||||||
|
|
||||||
|
// Build model relations fields.
|
||||||
|
var fields: [rawDefinitionType.Struct.fields.len]std.builtin.Type.StructField = undefined;
|
||||||
|
inline for (rawDefinitionType.Struct.fields, &fields) |originalField, *field| {
|
||||||
|
field.* = .{
|
||||||
|
.name = originalField.name,
|
||||||
|
.type = _relations.ModelRelation,
|
||||||
|
.default_value = null,
|
||||||
|
.is_comptime = false,
|
||||||
|
.alignment = @alignOf(_relations.ModelRelation),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return built type.
|
||||||
|
return @Type(.{
|
||||||
|
.Struct = std.builtin.Type.Struct{
|
||||||
|
.layout = std.builtin.Type.ContainerLayout.auto,
|
||||||
|
.fields = &fields,
|
||||||
|
.decls = &[_]std.builtin.Type.Declaration{},
|
||||||
|
.is_tuple = false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Repository of structures of a certain type.
|
/// Repository of structures of a certain type.
|
||||||
pub fn Repository(comptime Model: type, comptime TableShape: type, comptime config: RepositoryConfiguration(Model, TableShape)) type {
|
pub fn Repository(comptime Model: type, comptime TableShape: type, comptime repositoryConfig: RepositoryConfiguration(Model, TableShape)) type {
|
||||||
return struct {
|
return struct {
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
|
pub const ModelType = Model;
|
||||||
|
pub const TableType = TableShape;
|
||||||
|
pub const config = repositoryConfig;
|
||||||
|
|
||||||
pub const Query: type = query.RepositoryQuery(Model, TableShape, config);
|
pub const Query: type = query.RepositoryQuery(Model, TableShape, config);
|
||||||
pub const Insert: type = insert.RepositoryInsert(Model, TableShape, config, config.insertShape);
|
pub const Insert: type = insert.RepositoryInsert(Model, TableShape, config, config.insertShape);
|
||||||
|
|
||||||
/// Type of one model key.
|
/// Type of one model key.
|
||||||
pub const KeyType = ModelKeyType(Model, TableShape, config);
|
pub const KeyType = ModelKeyType(Model, TableShape, config);
|
||||||
|
|
||||||
|
pub const relations = struct {
|
||||||
|
/// Make a "one to one" relation.
|
||||||
|
pub fn one(comptime toRepo: anytype, comptime oneConfig: _relations.OneConfiguration) type {
|
||||||
|
return _relations.one(Self, toRepo, oneConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make a "one to many" or "many to many" relation.
|
||||||
|
pub fn many(comptime toRepo: anytype, comptime manyConfig: _relations.ManyConfiguration) type {
|
||||||
|
return _relations.many(Self, toRepo, manyConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Define a relations object for a repository.
|
||||||
|
pub fn define(rawDefinition: anytype) RelationsDefinitionType(rawDefinition) {
|
||||||
|
const rawDefinitionType = @TypeOf(rawDefinition);
|
||||||
|
|
||||||
|
// Initialize final relations definition.
|
||||||
|
var definition: RelationsDefinitionType(rawDefinition) = undefined;
|
||||||
|
|
||||||
|
// Check that the definition structure only include known fields.
|
||||||
|
inline for (std.meta.fieldNames(rawDefinitionType)) |fieldName| {
|
||||||
|
if (!@hasField(Model, fieldName)) {
|
||||||
|
@compileError("No corresponding field for relation " ++ fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alter definition structure to add the field name.
|
||||||
|
@field(definition, fieldName) = .{
|
||||||
|
.relation = @field(rawDefinition, fieldName),
|
||||||
|
.field = fieldName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return altered definition structure.
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub fn InsertCustom(comptime InsertShape: type) type {
|
pub fn InsertCustom(comptime InsertShape: type) type {
|
||||||
return insert.RepositoryInsert(Model, TableShape, config, InsertShape);
|
return insert.RepositoryInsert(Model, TableShape, config, InsertShape);
|
||||||
}
|
}
|
||||||
|
@ -102,76 +170,7 @@ pub fn Repository(comptime Model: type, comptime TableShape: type, comptime conf
|
||||||
var modelQuery = Self.Query.init(allocator, connector, .{});
|
var modelQuery = Self.Query.init(allocator, connector, .{});
|
||||||
defer modelQuery.deinit();
|
defer modelQuery.deinit();
|
||||||
|
|
||||||
if (config.key.len == 1) {
|
try modelQuery.whereKey(modelKey);
|
||||||
// Find key name and its type.
|
|
||||||
const keyName = config.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 modelQuery.whereValue(KeyType, keyName, "=", modelKey)
|
|
||||||
else
|
|
||||||
// Otherwise, use it as an array.
|
|
||||||
try modelQuery.whereIn(keyType, keyName, modelKey);
|
|
||||||
},
|
|
||||||
// Add a simple condition with the pointed value.
|
|
||||||
else => try modelQuery.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 modelQuery.whereValue(KeyType, keyName, "=", modelKey)
|
|
||||||
else
|
|
||||||
// Otherwise, use it as an array.
|
|
||||||
try modelQuery.whereIn(keyType, keyName, modelKey);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Add a simple condition with the given value.
|
|
||||||
else => try modelQuery.whereValue(keyType, keyName, "=", modelKey),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Accept arrays / slices of keys, and simple keys.
|
|
||||||
// Uniformize modelKey parameter to a slice.
|
|
||||||
const modelKeysList: []const Self.KeyType = switch (@typeInfo(@TypeOf(modelKey))) {
|
|
||||||
.Pointer => |ptr| switch (ptr.size) {
|
|
||||||
.One => switch (@typeInfo(ptr.child)) {
|
|
||||||
// Already an array.
|
|
||||||
.Array => @as([]const Self.KeyType, modelKey),
|
|
||||||
// Convert the pointer to an array.
|
|
||||||
else => &[1]Self.KeyType{@as(Self.KeyType, modelKey.*)},
|
|
||||||
},
|
|
||||||
// Already a slice.
|
|
||||||
else => @as([]const Self.KeyType, modelKey),
|
|
||||||
},
|
|
||||||
// Convert the value to an array.
|
|
||||||
else => &[1]Self.KeyType{@as(Self.KeyType, modelKey)},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize keys conditions list.
|
|
||||||
const conditions: []_sql.SqlParams = try allocator.alloc(_sql.SqlParams, modelKeysList.len);
|
|
||||||
defer allocator.free(conditions);
|
|
||||||
|
|
||||||
// For each model key, add its conditions.
|
|
||||||
for (modelKeysList, conditions) |_modelKey, *condition| {
|
|
||||||
condition.* = try modelQuery.newCondition().@"and"(
|
|
||||||
&try buildCompositeKeysConditions(TableShape, config.key, modelQuery.newCondition(), _modelKey)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set WHERE conditions in the query with all keys conditions.
|
|
||||||
modelQuery.where(try modelQuery.newCondition().@"or"(conditions));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute query and return its result.
|
// Execute query and return its result.
|
||||||
return try modelQuery.get(allocator);
|
return try modelQuery.get(allocator);
|
||||||
|
@ -210,7 +209,7 @@ pub fn Repository(comptime Model: type, comptime TableShape: type, comptime conf
|
||||||
updateQuery.returningAll();
|
updateQuery.returningAll();
|
||||||
|
|
||||||
// Initialize conditions array.
|
// Initialize conditions array.
|
||||||
var conditions: [config.key.len]_sql.SqlParams = undefined;
|
var conditions: [config.key.len]_sql.RawQuery = undefined;
|
||||||
inline for (config.key, &conditions) |keyName, *condition| {
|
inline for (config.key, &conditions) |keyName, *condition| {
|
||||||
// Add a where condition for each key.
|
// Add a where condition for each key.
|
||||||
condition.* = try updateQuery.newCondition().value(@TypeOf(@field(modelSql, keyName)), keyName, "=", @field(modelSql, keyName));
|
condition.* = try updateQuery.newCondition().value(@TypeOf(@field(modelSql, keyName)), keyName, "=", @field(modelSql, keyName));
|
||||||
|
@ -271,24 +270,3 @@ pub fn RepositoryResult(comptime Model: type) type {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.SqlParams {
|
|
||||||
// Conditions list for all keys in the composite key.
|
|
||||||
var conditions: [keys.len]_sql.SqlParams = 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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -12,8 +12,10 @@ pub const RepositoryResult = repository.RepositoryResult;
|
||||||
|
|
||||||
pub const Insertable = insert.Insertable;
|
pub const Insertable = insert.Insertable;
|
||||||
|
|
||||||
pub const QueryParameter = _sql.QueryParameter;
|
pub const relations = @import("relations.zig");
|
||||||
pub const SqlParams = _sql.SqlParams;
|
|
||||||
|
pub const RawQueryParameter = _sql.RawQueryParameter;
|
||||||
|
pub const RawQuery = _sql.RawQuery;
|
||||||
|
|
||||||
pub const database = @import("database.zig");
|
pub const database = @import("database.zig");
|
||||||
pub const Session = session.Session;
|
pub const Session = session.Session;
|
||||||
|
|
337
src/sql.zig
337
src/sql.zig
|
@ -2,9 +2,67 @@ const std = @import("std");
|
||||||
const errors = @import("errors.zig");
|
const errors = @import("errors.zig");
|
||||||
|
|
||||||
/// A structure with SQL and its parameters.
|
/// A structure with SQL and its parameters.
|
||||||
pub const SqlParams = struct {
|
pub const RawQuery = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
sql: []const u8,
|
sql: []const u8,
|
||||||
params: []const QueryParameter,
|
params: []const RawQueryParameter,
|
||||||
|
|
||||||
|
/// Build an SQL query with all the given query parts, separated by a space.
|
||||||
|
pub fn fromConcat(allocator: std.mem.Allocator, queries: []const RawQuery) !Self {
|
||||||
|
// Allocate an array with all SQL queries.
|
||||||
|
const queriesSql = try allocator.alloc([]const u8, queries.len);
|
||||||
|
defer allocator.free(queriesSql);
|
||||||
|
|
||||||
|
// Allocate an array with all parameters arrays.
|
||||||
|
const queriesParams = try allocator.alloc([]const RawQueryParameter, queries.len);
|
||||||
|
defer allocator.free(queriesSql);
|
||||||
|
|
||||||
|
// Fill SQL queries and parameters arrays.
|
||||||
|
for (queries, queriesSql, queriesParams) |_query, *_querySql, *_queryParam| {
|
||||||
|
_querySql.* = _query.sql;
|
||||||
|
_queryParam.* = _query.params;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build final query with its parameters.
|
||||||
|
return Self{
|
||||||
|
.sql = try std.mem.join(allocator, " ", queriesSql),
|
||||||
|
.params = try std.mem.concat(allocator, RawQueryParameter, queriesParams),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a full SQL query with numbered parameters.
|
||||||
|
pub fn build(self: Self, allocator: std.mem.Allocator) ![]u8 {
|
||||||
|
if (self.params.len <= 0) {
|
||||||
|
// No parameters, just copy SQL.
|
||||||
|
return allocator.dupe(u8, self.sql);
|
||||||
|
} else {
|
||||||
|
// Copy SQL and replace '?' by numbered parameters.
|
||||||
|
const sqlSize = self.sql.len + computeRequiredSpaceForNumbers(self.params.len);
|
||||||
|
var sqlBuf = try std.ArrayList(u8).initCapacity(allocator, sqlSize);
|
||||||
|
defer sqlBuf.deinit();
|
||||||
|
|
||||||
|
// Parameter counter.
|
||||||
|
var currentParameter: usize = 1;
|
||||||
|
|
||||||
|
for (self.sql) |char| {
|
||||||
|
// Copy each character but '?', replaced by the current parameter string.
|
||||||
|
|
||||||
|
if (char == '?') {
|
||||||
|
// Copy the parameter string in place of '?'.
|
||||||
|
try sqlBuf.writer().print("${d}", .{currentParameter});
|
||||||
|
// Increment parameter count.
|
||||||
|
currentParameter += 1;
|
||||||
|
} else {
|
||||||
|
// Simply pass the current character.
|
||||||
|
try sqlBuf.append(char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the built SQL query.
|
||||||
|
return sqlBuf.toOwnedSlice();
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Generate parameters SQL in the form of "?,?,?,?"
|
/// Generate parameters SQL in the form of "?,?,?,?"
|
||||||
|
@ -21,6 +79,143 @@ pub fn generateParametersSql(allocator: std.mem.Allocator, parametersCount: u64)
|
||||||
return sql;
|
return sql;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compute required string size of numbers for the given parameters count.
|
||||||
|
pub fn computeRequiredSpaceForNumbers(parametersCount: usize) usize {
|
||||||
|
var numbersSize: usize = 0; // Initialize the required size.
|
||||||
|
var remaining = parametersCount; // Initialize the remaining parameters to count.
|
||||||
|
var currentSliceSize: usize = 9; // Initialize the first slice size of numbers.
|
||||||
|
var i: usize = 1; // Initialize the current slice count.
|
||||||
|
|
||||||
|
while (remaining > 0) {
|
||||||
|
// Compute the count of numbers in the current slice.
|
||||||
|
const numbersCount = @min(remaining, currentSliceSize);
|
||||||
|
// Add the required string size of all numbers in this slice.
|
||||||
|
numbersSize += i * numbersCount;
|
||||||
|
// Subtract the counted numbers in this current slice.
|
||||||
|
remaining -= numbersCount;
|
||||||
|
// Move to the next slice.
|
||||||
|
i += 1;
|
||||||
|
currentSliceSize *= 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the computed numbers size.
|
||||||
|
return numbersSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute required string size for the given parameter number.
|
||||||
|
pub fn computeRequiredSpaceForParameter(parameterNumber: usize) !usize {
|
||||||
|
var i: usize = 1;
|
||||||
|
while (parameterNumber >= try std.math.powi(usize, 10, i)) {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A query parameter.
|
||||||
|
pub const RawQueryParameter = union(enum) {
|
||||||
|
string: []const u8,
|
||||||
|
integer: i64,
|
||||||
|
number: f64,
|
||||||
|
bool: bool,
|
||||||
|
null: void,
|
||||||
|
|
||||||
|
/// Convert any value to a query parameter.
|
||||||
|
pub fn fromValue(value: anytype) errors.ZrmError!RawQueryParameter {
|
||||||
|
// Get given value type.
|
||||||
|
const valueType = @typeInfo(@TypeOf(value));
|
||||||
|
|
||||||
|
return switch (valueType) {
|
||||||
|
.Int, .ComptimeInt => return .{ .integer = @intCast(value), },
|
||||||
|
.Float, .ComptimeFloat => return .{ .number = @floatCast(value), },
|
||||||
|
.Bool => return .{ .bool = value, },
|
||||||
|
.Null => return .{ .null = true, },
|
||||||
|
.Pointer => |pointer| {
|
||||||
|
if (pointer.size == .One) {
|
||||||
|
// Get pointed value.
|
||||||
|
return RawQueryParameter.fromValue(value.*);
|
||||||
|
} else {
|
||||||
|
// Can only take an array of u8 (= string).
|
||||||
|
if (pointer.child == u8) {
|
||||||
|
return .{ .string = value };
|
||||||
|
} else {
|
||||||
|
return errors.ZrmError.UnsupportedTableType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Enum, .EnumLiteral => {
|
||||||
|
return .{ .string = @tagName(value) };
|
||||||
|
},
|
||||||
|
.Optional => {
|
||||||
|
if (value) |val| {
|
||||||
|
// The optional value is defined, use it as a query parameter.
|
||||||
|
return RawQueryParameter.fromValue(val);
|
||||||
|
} else {
|
||||||
|
// If an optional value is not defined, set it to NULL.
|
||||||
|
return .{ .null = true };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => return errors.ZrmError.UnsupportedTableType
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/// SELECT query part builder for a given table.
|
||||||
|
pub fn SelectBuilder(comptime TableShape: type) type {
|
||||||
|
// Get fields count in the table shape.
|
||||||
|
const columnsCount = @typeInfo(TableShape).Struct.fields.len;
|
||||||
|
|
||||||
|
// Sum of lengths of all selected columns formats.
|
||||||
|
var _selectColumnsLength = 0;
|
||||||
|
|
||||||
|
const selectColumns = comptime select: {
|
||||||
|
// Initialize the select columns array.
|
||||||
|
var _select: [columnsCount][]const u8 = undefined;
|
||||||
|
|
||||||
|
// For each field, generate a format string.
|
||||||
|
for (@typeInfo(TableShape).Struct.fields, &_select) |field, *columnSelect| {
|
||||||
|
// Select the current field column.
|
||||||
|
columnSelect.* = "\"{s}\".\"" ++ field.name ++ "\" AS \"{s}" ++ field.name ++ "\"";
|
||||||
|
_selectColumnsLength = _selectColumnsLength + columnSelect.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
break :select _select;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export computed select columns length.
|
||||||
|
const selectColumnsLength = _selectColumnsLength;
|
||||||
|
|
||||||
|
return struct {
|
||||||
|
/// Build a SELECT query part for a given table, renaming columns with the given prefix.
|
||||||
|
pub fn build(allocator: std.mem.Allocator, table: []const u8, prefix: []const u8) ![]const u8 {
|
||||||
|
// Initialize full select string with precomputed size.
|
||||||
|
var fullSelect = try std.ArrayList(u8).initCapacity(allocator,
|
||||||
|
selectColumnsLength // static SQL size.
|
||||||
|
+ columnsCount*(table.len - 2 + prefix.len - 2) // replacing %s and %s by table and prefix.
|
||||||
|
+ (columnsCount - 1) * 2 // ", "
|
||||||
|
);
|
||||||
|
defer fullSelect.deinit();
|
||||||
|
|
||||||
|
var first = true;
|
||||||
|
inline for (selectColumns) |columnSelect| {
|
||||||
|
// Add ", " between all selected columns.
|
||||||
|
if (first) {
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
try fullSelect.appendSlice(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
try fullSelect.writer().print(columnSelect, .{table, prefix});
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullSelect.toOwnedSlice(); // Return built full select.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// Compute required string size of numbers for the given parameters count, with taking in account the already used parameters numbers.
|
/// Compute required string size of numbers for the given parameters count, with taking in account the already used parameters numbers.
|
||||||
pub fn computeRequiredSpaceForParametersNumbers(parametersCount: usize, alreadyUsedParameters: usize) usize {
|
pub fn computeRequiredSpaceForParametersNumbers(parametersCount: usize, alreadyUsedParameters: usize) usize {
|
||||||
var remainingUsedParameters = alreadyUsedParameters; // Initialize the count of used parameters to mark as taken.
|
var remainingUsedParameters = alreadyUsedParameters; // Initialize the count of used parameters to mark as taken.
|
||||||
|
@ -55,149 +250,25 @@ pub fn computeRequiredSpaceForParametersNumbers(parametersCount: usize, alreadyU
|
||||||
return numbersSize;
|
return numbersSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute required string size of numbers for the given parameters count.
|
/// Copy the given source query and replace '?' parameters by numbered parameters.
|
||||||
pub fn computeRequiredSpaceForNumbers(parametersCount: usize) usize {
|
pub fn copyAndReplaceSqlParameters(currentParameter: *usize, parametersCount: usize, writer: std.ArrayList(u8).Writer, source: []const u8) !void {
|
||||||
var numbersSize: usize = 0; // Initialize the required size.
|
|
||||||
var remaining = parametersCount; // Initialize the remaining parameters to count.
|
|
||||||
var currentSliceSize: usize = 9; // Initialize the first slice size of numbers.
|
|
||||||
var i: usize = 1; // Initialize the current slice count.
|
|
||||||
|
|
||||||
while (remaining > 0) {
|
|
||||||
// Compute the count of numbers in the current slice.
|
|
||||||
const numbersCount = @min(remaining, currentSliceSize);
|
|
||||||
// Add the required string size of all numbers in this slice.
|
|
||||||
numbersSize += i * numbersCount;
|
|
||||||
// Subtract the counted numbers in this current slice.
|
|
||||||
remaining -= numbersCount;
|
|
||||||
// Move to the next slice.
|
|
||||||
i += 1;
|
|
||||||
currentSliceSize *= 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the computed numbers size.
|
|
||||||
return numbersSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute required string size for the given parameter number.
|
|
||||||
pub fn computeRequiredSpaceForParameter(parameterNumber: usize) !usize {
|
|
||||||
var i: usize = 1;
|
|
||||||
while (parameterNumber >= try std.math.powi(usize, 10, i)) {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn copyAndReplaceSqlParameters(currentParameter: *usize, parametersCount: usize, dest: []u8, source: []const u8) !void {
|
|
||||||
// If there are no parameters, just copy source SQL.
|
// If there are no parameters, just copy source SQL.
|
||||||
if (parametersCount <= 0) {
|
if (parametersCount <= 0) {
|
||||||
std.mem.copyForwards(u8, dest, source);
|
try writer.writeAll(source);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Current dest cursor.
|
|
||||||
var destCursor: usize = 0;
|
|
||||||
|
|
||||||
for (source) |char| {
|
for (source) |char| {
|
||||||
// Copy each character but '?', replaced by the current parameter string.
|
// Copy each character but '?', replaced by the current parameter string.
|
||||||
|
|
||||||
if (char == '?') {
|
if (char == '?') {
|
||||||
// Create the parameter string.
|
|
||||||
const paramSize = 1 + try computeRequiredSpaceForParameter(currentParameter.*);
|
|
||||||
// Copy the parameter string in place of '?'.
|
// Copy the parameter string in place of '?'.
|
||||||
_ = try std.fmt.bufPrint(dest[destCursor..destCursor+paramSize], "${d}", .{currentParameter.*});
|
try writer.print("${d}", .{currentParameter.*});
|
||||||
// Add parameter string length to the current query cursor.
|
|
||||||
destCursor += paramSize;
|
|
||||||
// Increment parameter count.
|
// Increment parameter count.
|
||||||
currentParameter.* += 1;
|
currentParameter.* += 1;
|
||||||
} else {
|
} else {
|
||||||
// Simply pass the current character.
|
// Simply write the current character.
|
||||||
dest[destCursor] = char;
|
try writer.writeByte(char);
|
||||||
destCursor += 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn numberSqlParameters(sql: []const u8, comptime parametersCount: usize) [sql.len + computeRequiredSpaceForNumbers(parametersCount)]u8 {
|
|
||||||
// If there are no parameters, just return built SQL.
|
|
||||||
if (parametersCount <= 0) {
|
|
||||||
return @as([sql.len]u8, sql[0..sql.len].*);
|
|
||||||
}
|
|
||||||
|
|
||||||
// New query buffer.
|
|
||||||
var query: [sql.len + computeRequiredSpaceForNumbers(parametersCount)]u8 = undefined;
|
|
||||||
|
|
||||||
// Current query cursor.
|
|
||||||
var queryCursor: usize = 0;
|
|
||||||
// Current parameter count.
|
|
||||||
var currentParameter: usize = 1;
|
|
||||||
|
|
||||||
for (sql) |char| {
|
|
||||||
// Copy each character but '?', replaced by the current parameter string.
|
|
||||||
|
|
||||||
if (char == '?') {
|
|
||||||
var buffer: [computeRequiredSpaceForParameter(currentParameter)]u8 = undefined;
|
|
||||||
// Create the parameter string.
|
|
||||||
const paramStr = try std.fmt.bufPrint(&buffer, "${d}", .{currentParameter});
|
|
||||||
// Copy the parameter string in place of '?'.
|
|
||||||
@memcpy(query[queryCursor..(queryCursor + paramStr.len)], paramStr);
|
|
||||||
// Add parameter string length to the current query cursor.
|
|
||||||
queryCursor += paramStr.len;
|
|
||||||
// Increment parameter count.
|
|
||||||
currentParameter += 1;
|
|
||||||
} else {
|
|
||||||
// Simply pass the current character.
|
|
||||||
query[queryCursor] = char;
|
|
||||||
queryCursor += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return built query.
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A query parameter.
|
|
||||||
pub const QueryParameter = union(enum) {
|
|
||||||
string: []const u8,
|
|
||||||
integer: i64,
|
|
||||||
number: f64,
|
|
||||||
bool: bool,
|
|
||||||
null: void,
|
|
||||||
|
|
||||||
/// Convert any value to a query parameter.
|
|
||||||
pub fn fromValue(value: anytype) errors.ZrmError!QueryParameter {
|
|
||||||
// Get given value type.
|
|
||||||
const valueType = @typeInfo(@TypeOf(value));
|
|
||||||
|
|
||||||
return switch (valueType) {
|
|
||||||
.Int, .ComptimeInt => return .{ .integer = @intCast(value), },
|
|
||||||
.Float, .ComptimeFloat => return .{ .number = @floatCast(value), },
|
|
||||||
.Bool => return .{ .bool = value, },
|
|
||||||
.Null => return .{ .null = true, },
|
|
||||||
.Pointer => |pointer| {
|
|
||||||
if (pointer.size == .One) {
|
|
||||||
// Get pointed value.
|
|
||||||
return QueryParameter.fromValue(value.*);
|
|
||||||
} else {
|
|
||||||
// Can only take an array of u8 (= string).
|
|
||||||
if (pointer.child == u8) {
|
|
||||||
return .{ .string = value };
|
|
||||||
} else {
|
|
||||||
return errors.ZrmError.UnsupportedTableType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.Enum, .EnumLiteral => {
|
|
||||||
return .{ .string = @tagName(value) };
|
|
||||||
},
|
|
||||||
.Optional => {
|
|
||||||
if (value) |val| {
|
|
||||||
// The optional value is defined, use it as a query parameter.
|
|
||||||
return QueryParameter.fromValue(val);
|
|
||||||
} else {
|
|
||||||
// If an optional value is not defined, set it to NULL.
|
|
||||||
return .{ .null = true };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
else => return errors.ZrmError.UnsupportedTableType
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
@ -12,8 +12,8 @@ const repository = @import("repository.zig");
|
||||||
pub fn RepositoryUpdateConfiguration(comptime UpdateShape: type) type {
|
pub fn RepositoryUpdateConfiguration(comptime UpdateShape: type) type {
|
||||||
return struct {
|
return struct {
|
||||||
value: ?UpdateShape = null,
|
value: ?UpdateShape = null,
|
||||||
where: ?_sql.SqlParams = null,
|
where: ?_sql.RawQuery = null,
|
||||||
returning: ?_sql.SqlParams = null,
|
returning: ?_sql.RawQuery = null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ pub fn RepositoryUpdate(comptime Model: type, comptime TableShape: type, comptim
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set WHERE conditions.
|
/// Set WHERE conditions.
|
||||||
pub fn where(self: *Self, _where: _sql.SqlParams) void {
|
pub fn where(self: *Self, _where: _sql.RawQuery) void {
|
||||||
self.updateConfig.where = _where;
|
self.updateConfig.where = _where;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,7 +144,7 @@ pub fn RepositoryUpdate(comptime Model: type, comptime TableShape: type, comptim
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set selected columns for RETURNING clause.
|
/// Set selected columns for RETURNING clause.
|
||||||
pub fn returning(self: *Self, _select: _sql.SqlParams) void {
|
pub fn returning(self: *Self, _select: _sql.RawQuery) void {
|
||||||
self.updateConfig.returning = _select;
|
self.updateConfig.returning = _select;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,7 +157,7 @@ pub fn RepositoryUpdate(comptime Model: type, comptime TableShape: type, comptim
|
||||||
self.returning(.{
|
self.returning(.{
|
||||||
// Join selected columns.
|
// Join selected columns.
|
||||||
.sql = std.mem.join(self.arena.allocator(), ", ", _select),
|
.sql = std.mem.join(self.arena.allocator(), ", ", _select),
|
||||||
.params = &[_]_sql.QueryParameter{}, // No parameters.
|
.params = &[_]_sql.RawQueryParameter{}, // No parameters.
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,7 +165,7 @@ pub fn RepositoryUpdate(comptime Model: type, comptime TableShape: type, comptim
|
||||||
pub fn returningAll(self: *Self) void {
|
pub fn returningAll(self: *Self) void {
|
||||||
self.returning(.{
|
self.returning(.{
|
||||||
.sql = "*",
|
.sql = "*",
|
||||||
.params = &[_]_sql.QueryParameter{}, // No parameters.
|
.params = &[_]_sql.RawQueryParameter{}, // No parameters.
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,61 +203,53 @@ pub fn RepositoryUpdate(comptime Model: type, comptime TableShape: type, comptim
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allocate SQL buffer from computed size.
|
// Allocate SQL buffer from computed size.
|
||||||
const sqlBuf = try self.arena.allocator().alloc(u8, fixedSqlSize
|
var sqlBuf = try std.ArrayList(u8).initCapacity(self.arena.allocator(), fixedSqlSize
|
||||||
+ (setSize)
|
+ (setSize)
|
||||||
+ (whereSize)
|
+ (whereSize)
|
||||||
+ (returningSize)
|
+ (returningSize)
|
||||||
);
|
);
|
||||||
|
defer sqlBuf.deinit();
|
||||||
// Fill SQL buffer.
|
|
||||||
|
|
||||||
// Restart parameter counter at 1.
|
// Restart parameter counter at 1.
|
||||||
currentParameter = 1;
|
currentParameter = 1;
|
||||||
|
|
||||||
// SQL query initialisation.
|
// SQL query initialisation.
|
||||||
@memcpy(sqlBuf[0..sqlBase.len], sqlBase);
|
try sqlBuf.appendSlice(sqlBase);
|
||||||
var sqlBufCursor: usize = sqlBase.len;
|
|
||||||
|
|
||||||
// Add SET columns values.
|
// Add SET columns values.
|
||||||
inline for (columns) |column| {
|
inline for (columns) |column| {
|
||||||
// Create the SET string and append it to the SQL buffer.
|
// Create the SET string and append it to the SQL buffer.
|
||||||
const setColumnSize = column.len + 1 + 1 + try _sql.computeRequiredSpaceForParameter(currentParameter) + 1;
|
try sqlBuf.writer().print("{s}=${d},", .{column, currentParameter});
|
||||||
_ = try std.fmt.bufPrint(sqlBuf[sqlBufCursor..sqlBufCursor+setColumnSize], "{s}=${d},", .{column, currentParameter});
|
|
||||||
sqlBufCursor += setColumnSize;
|
|
||||||
// Increment parameter count.
|
// Increment parameter count.
|
||||||
currentParameter += 1;
|
currentParameter += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overwrite the last ','.
|
// Overwrite the last ','.
|
||||||
sqlBufCursor -= 1;
|
_ = sqlBuf.pop();
|
||||||
|
|
||||||
// WHERE clause.
|
// WHERE clause.
|
||||||
if (self.updateConfig.where) |_where| {
|
if (self.updateConfig.where) |_where| {
|
||||||
@memcpy(sqlBuf[sqlBufCursor..sqlBufCursor+(1 + whereClause.len + 1)], " " ++ whereClause ++ " ");
|
try sqlBuf.appendSlice(" " ++ whereClause ++ " ");
|
||||||
// Copy WHERE clause content and replace parameters, if there are some.
|
// Copy WHERE clause content and replace parameters, if there are some.
|
||||||
try _sql.copyAndReplaceSqlParameters(¤tParameter,
|
try _sql.copyAndReplaceSqlParameters(¤tParameter,
|
||||||
_where.params.len,
|
_where.params.len, sqlBuf.writer(), _where.sql
|
||||||
sqlBuf[sqlBufCursor+(1+whereClause.len+1)..sqlBufCursor+whereSize], _where.sql
|
|
||||||
);
|
);
|
||||||
sqlBufCursor += whereSize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append RETURNING clause, if there is one defined.
|
// Append RETURNING clause, if there is one defined.
|
||||||
if (self.updateConfig.returning) |_returning| {
|
if (self.updateConfig.returning) |_returning| {
|
||||||
@memcpy(sqlBuf[sqlBufCursor..sqlBufCursor+(1 + returningClause.len + 1)], " " ++ returningClause ++ " ");
|
try sqlBuf.appendSlice(" " ++ returningClause ++ " ");
|
||||||
// Copy RETURNING clause content and replace parameters, if there are some.
|
// Copy RETURNING clause content and replace parameters, if there are some.
|
||||||
try _sql.copyAndReplaceSqlParameters(¤tParameter,
|
try _sql.copyAndReplaceSqlParameters(¤tParameter,
|
||||||
_returning.params.len,
|
_returning.params.len, sqlBuf.writer(), _returning.sql
|
||||||
sqlBuf[sqlBufCursor+(1+returningClause.len+1)..sqlBufCursor+returningSize], _returning.sql
|
|
||||||
);
|
);
|
||||||
sqlBufCursor += returningSize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ";" to end the query.
|
// ";" to end the query.
|
||||||
sqlBuf[sqlBufCursor] = ';'; sqlBufCursor += 1;
|
try sqlBuf.append(';');
|
||||||
|
|
||||||
// Save built SQL query.
|
// Save built SQL query.
|
||||||
self.sql = sqlBuf;
|
self.sql = try sqlBuf.toOwnedSlice();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute the update query.
|
/// Execute the update query.
|
||||||
|
|
|
@ -2,18 +2,29 @@
|
||||||
DROP SCHEMA public CASCADE;
|
DROP SCHEMA public CASCADE;
|
||||||
CREATE SCHEMA public;
|
CREATE SCHEMA public;
|
||||||
|
|
||||||
-- Create default models table.
|
-- Create models table.
|
||||||
CREATE TABLE models (
|
CREATE TABLE models (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
name VARCHAR NOT NULL,
|
name VARCHAR NOT NULL,
|
||||||
amount NUMERIC(12, 2) NOT NULL
|
amount NUMERIC(12, 2) NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- Create submodels table.
|
||||||
|
CREATE TABLE submodels (
|
||||||
|
uuid UUID PRIMARY KEY,
|
||||||
|
label VARCHAR NOT NULL,
|
||||||
|
parent_id INT NULL,
|
||||||
|
FOREIGN KEY (parent_id) REFERENCES models ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX submodels_parent_id_index ON submodels(parent_id);
|
||||||
|
|
||||||
-- Insert default data.
|
-- Insert default data.
|
||||||
INSERT INTO models(name, amount) VALUES ('test', 50);
|
INSERT INTO models(name, amount) VALUES ('test', 50);
|
||||||
INSERT INTO models(name, amount) VALUES ('updatable', 33.12);
|
INSERT INTO models(name, amount) VALUES ('updatable', 33.12);
|
||||||
|
INSERT INTO submodels(uuid, label, parent_id) VALUES ('f6868a5b-2efc-455f-b76e-872df514404f', 'test', 1);
|
||||||
|
INSERT INTO submodels(uuid, label, parent_id) VALUES ('013ef171-9781-40e9-b843-f6bc11890070', 'another', 1);
|
||||||
|
|
||||||
-- Create default composite models table.
|
-- Create composite models table.
|
||||||
CREATE TABLE composite_models (
|
CREATE TABLE composite_models (
|
||||||
firstcol SERIAL NOT NULL,
|
firstcol SERIAL NOT NULL,
|
||||||
secondcol VARCHAR NOT NULL,
|
secondcol VARCHAR NOT NULL,
|
||||||
|
|
|
@ -39,9 +39,9 @@ test "zrm.conditions combined" {
|
||||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||||
defer arena.deinit();
|
defer arena.deinit();
|
||||||
|
|
||||||
const condition = try zrm.conditions.@"and"(arena.allocator(), &[_]zrm.SqlParams{
|
const condition = try zrm.conditions.@"and"(arena.allocator(), &[_]zrm.RawQuery{
|
||||||
try zrm.conditions.value(usize, arena.allocator(), "test", "=", 5),
|
try zrm.conditions.value(usize, arena.allocator(), "test", "=", 5),
|
||||||
try zrm.conditions.@"or"(arena.allocator(), &[_]zrm.SqlParams{
|
try zrm.conditions.@"or"(arena.allocator(), &[_]zrm.RawQuery{
|
||||||
try zrm.conditions.in(usize, arena.allocator(), "intest", &[_]usize{2, 3, 8}),
|
try zrm.conditions.in(usize, arena.allocator(), "intest", &[_]usize{2, 3, 8}),
|
||||||
try zrm.conditions.column(arena.allocator(), "firstcol", "<>", "secondcol"),
|
try zrm.conditions.column(arena.allocator(), "firstcol", "<>", "secondcol"),
|
||||||
}),
|
}),
|
||||||
|
|
54
tests/relations.zig
Normal file
54
tests/relations.zig
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const pg = @import("pg");
|
||||||
|
const zrm = @import("zrm");
|
||||||
|
const repository = @import("repository.zig");
|
||||||
|
|
||||||
|
/// PostgreSQL database connection.
|
||||||
|
var database: *pg.Pool = undefined;
|
||||||
|
|
||||||
|
/// Initialize database connection.
|
||||||
|
fn initDatabase(allocator: std.mem.Allocator) !void {
|
||||||
|
database = try pg.Pool.init(allocator, .{
|
||||||
|
.connect = .{
|
||||||
|
.host = "localhost",
|
||||||
|
.port = 5432,
|
||||||
|
},
|
||||||
|
.auth = .{
|
||||||
|
.username = "zrm",
|
||||||
|
.password = "zrm",
|
||||||
|
.database = "zrm",
|
||||||
|
},
|
||||||
|
.size = 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test "belongsTo" {
|
||||||
|
zrm.setDebug(true);
|
||||||
|
|
||||||
|
try initDatabase(std.testing.allocator);
|
||||||
|
defer database.deinit();
|
||||||
|
var poolConnector = zrm.database.PoolConnector{
|
||||||
|
.pool = database,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build a query of submodels.
|
||||||
|
var myQuery = repository.MySubmodelRepository.Query.init(std.testing.allocator, poolConnector.connector(), .{});
|
||||||
|
defer myQuery.deinit();
|
||||||
|
// Retrieve parents of submodels from relation.
|
||||||
|
try myQuery.with(repository.MySubmodelRelations.parent);
|
||||||
|
|
||||||
|
try myQuery.buildSql();
|
||||||
|
|
||||||
|
// Get query result.
|
||||||
|
var result = try myQuery.get(std.testing.allocator);
|
||||||
|
defer result.deinit();
|
||||||
|
|
||||||
|
// Checking result.
|
||||||
|
try std.testing.expectEqual(2, result.models.len);
|
||||||
|
try std.testing.expectEqual(1, result.models[0].parent_id);
|
||||||
|
try std.testing.expectEqual(1, result.models[1].parent_id);
|
||||||
|
try std.testing.expectEqual(repository.MyModel, @TypeOf(result.models[0].parent.?));
|
||||||
|
try std.testing.expectEqual(repository.MyModel, @TypeOf(result.models[1].parent.?));
|
||||||
|
try std.testing.expectEqual(1, result.models[0].parent.?.id);
|
||||||
|
try std.testing.expectEqual(1, result.models[1].parent.?.id);
|
||||||
|
}
|
|
@ -26,9 +26,56 @@ const MySubmodel = struct {
|
||||||
uuid: []const u8,
|
uuid: []const u8,
|
||||||
label: []const u8,
|
label: []const u8,
|
||||||
|
|
||||||
|
parent_id: ?i32 = null,
|
||||||
parent: ?MyModel = null,
|
parent: ?MyModel = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MySubmodelTable = struct {
|
||||||
|
uuid: []const u8,
|
||||||
|
label: []const u8,
|
||||||
|
|
||||||
|
parent_id: ?i32 = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert an SQL row to a model.
|
||||||
|
fn submodelFromSql(raw: MySubmodelTable) !MySubmodel {
|
||||||
|
return .{
|
||||||
|
.uuid = raw.uuid,
|
||||||
|
.label = raw.label,
|
||||||
|
.parent_id = raw.parent_id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a model to an SQL row.
|
||||||
|
fn submodelToSql(model: MySubmodel) !MySubmodelTable {
|
||||||
|
return .{
|
||||||
|
.uuid = model.uuid,
|
||||||
|
.label = model.label,
|
||||||
|
.parent_id = model.parent_id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Declare a model repository.
|
||||||
|
pub const MySubmodelRepository = zrm.Repository(MySubmodel, MySubmodelTable, .{
|
||||||
|
.table = "submodels",
|
||||||
|
|
||||||
|
// Insert shape used by default for inserts in the repository.
|
||||||
|
.insertShape = MySubmodelTable,
|
||||||
|
|
||||||
|
.key = &[_][]const u8{"uuid"},
|
||||||
|
|
||||||
|
.fromSql = &submodelFromSql,
|
||||||
|
.toSql = &submodelToSql,
|
||||||
|
});
|
||||||
|
|
||||||
|
pub const MySubmodelRelations = MySubmodelRepository.relations.define(.{
|
||||||
|
.parent = MySubmodelRepository.relations.one(MyModelRepository, .{
|
||||||
|
.direct = .{
|
||||||
|
.foreignKey = "parent_id",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
/// An example model.
|
/// An example model.
|
||||||
pub const MyModel = struct {
|
pub const MyModel = struct {
|
||||||
id: i32,
|
id: i32,
|
||||||
|
@ -79,6 +126,14 @@ pub const MyModelRepository = zrm.Repository(MyModel, MyModelTable, .{
|
||||||
.toSql = &modelToSql,
|
.toSql = &modelToSql,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pub const MyModelRelations = MyModelRepository.relations.define(.{
|
||||||
|
.submodels = MyModelRepository.relations.many(MySubmodelRepository, .{
|
||||||
|
.direct = .{
|
||||||
|
.foreignKey = "parent_id",
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
test "model structures" {
|
test "model structures" {
|
||||||
// Initialize a test model.
|
// Initialize a test model.
|
||||||
|
@ -115,7 +170,7 @@ test "repository query SQL builder" {
|
||||||
try query.whereIn(usize, "id", &[_]usize{1, 2});
|
try query.whereIn(usize, "id", &[_]usize{1, 2});
|
||||||
try query.buildSql();
|
try query.buildSql();
|
||||||
|
|
||||||
const expectedSql = "SELECT * FROM models WHERE id IN ($1,$2);";
|
const expectedSql = "SELECT \"models\".* FROM \"models\" WHERE id IN ($1,$2);";
|
||||||
try std.testing.expectEqual(expectedSql.len, query.sql.?.len);
|
try std.testing.expectEqual(expectedSql.len, query.sql.?.len);
|
||||||
try std.testing.expectEqualStrings(expectedSql, query.sql.?);
|
try std.testing.expectEqualStrings(expectedSql, query.sql.?);
|
||||||
}
|
}
|
||||||
|
@ -138,7 +193,7 @@ test "repository element retrieval" {
|
||||||
try query.buildSql();
|
try query.buildSql();
|
||||||
|
|
||||||
// Check built SQL.
|
// Check built SQL.
|
||||||
const expectedSql = "SELECT * FROM models WHERE id = $1;";
|
const expectedSql = "SELECT \"models\".* FROM \"models\" WHERE id = $1;";
|
||||||
try std.testing.expectEqual(expectedSql.len, query.sql.?.len);
|
try std.testing.expectEqual(expectedSql.len, query.sql.?.len);
|
||||||
try std.testing.expectEqualStrings(expectedSql, query.sql.?);
|
try std.testing.expectEqualStrings(expectedSql, query.sql.?);
|
||||||
|
|
||||||
|
@ -166,11 +221,11 @@ test "repository complex SQL query" {
|
||||||
var query = MyModelRepository.Query.init(std.testing.allocator, poolConnector.connector(), .{});
|
var query = MyModelRepository.Query.init(std.testing.allocator, poolConnector.connector(), .{});
|
||||||
defer query.deinit();
|
defer query.deinit();
|
||||||
query.where(
|
query.where(
|
||||||
try query.newCondition().@"or"(&[_]zrm.SqlParams{
|
try query.newCondition().@"or"(&[_]zrm.RawQuery{
|
||||||
try query.newCondition().value(usize, "id", "=", 1),
|
try query.newCondition().value(usize, "id", "=", 1),
|
||||||
try query.newCondition().@"and"(&[_]zrm.SqlParams{
|
try query.newCondition().@"and"(&[_]zrm.RawQuery{
|
||||||
try query.newCondition().in(usize, "id", &[_]usize{100000, 200000, 300000}),
|
try query.newCondition().in(usize, "id", &[_]usize{100000, 200000, 300000}),
|
||||||
try query.newCondition().@"or"(&[_]zrm.SqlParams{
|
try query.newCondition().@"or"(&[_]zrm.RawQuery{
|
||||||
try query.newCondition().value(f64, "amount", ">", 12.13),
|
try query.newCondition().value(f64, "amount", ">", 12.13),
|
||||||
try query.newCondition().value([]const u8, "name", "=", "test"),
|
try query.newCondition().value([]const u8, "name", "=", "test"),
|
||||||
})
|
})
|
||||||
|
@ -179,7 +234,7 @@ test "repository complex SQL query" {
|
||||||
);
|
);
|
||||||
try query.buildSql();
|
try query.buildSql();
|
||||||
|
|
||||||
const expectedSql = "SELECT * FROM models WHERE (id = $1 OR (id IN ($2,$3,$4) AND (amount > $5 OR name = $6)));";
|
const expectedSql = "SELECT \"models\".* FROM \"models\" WHERE (id = $1 OR (id IN ($2,$3,$4) AND (amount > $5 OR name = $6)));";
|
||||||
try std.testing.expectEqual(expectedSql.len, query.sql.?.len);
|
try std.testing.expectEqual(expectedSql.len, query.sql.?.len);
|
||||||
try std.testing.expectEqualStrings(expectedSql, query.sql.?);
|
try std.testing.expectEqualStrings(expectedSql, query.sql.?);
|
||||||
|
|
||||||
|
|
|
@ -5,4 +5,5 @@ comptime {
|
||||||
_ = @import("repository.zig");
|
_ = @import("repository.zig");
|
||||||
_ = @import("composite.zig");
|
_ = @import("composite.zig");
|
||||||
_ = @import("sessions.zig");
|
_ = @import("sessions.zig");
|
||||||
|
_ = @import("relations.zig");
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue