+ Relations definition. + Add test submodels in testing database. * Generalize raw SQL query preparation and build. * Simplify and improve SQL query build algorithms.
274 lines
8.8 KiB
Zig
274 lines
8.8 KiB
Zig
const std = @import("std");
|
|
const errors = @import("errors.zig");
|
|
|
|
/// A structure with SQL and its parameters.
|
|
pub const RawQuery = struct {
|
|
const Self = @This();
|
|
|
|
sql: []const u8,
|
|
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 "?,?,?,?"
|
|
pub fn generateParametersSql(allocator: std.mem.Allocator, parametersCount: u64) ![]const u8 {
|
|
// Allocate required string size.
|
|
var sql: []u8 = try allocator.alloc(u8, parametersCount * 2 - 1);
|
|
for (0..parametersCount) |i| {
|
|
// Add a '?' for the current parameter.
|
|
sql[i*2] = '?';
|
|
// Add a ',' if it's not the last parameter.
|
|
if (i + 1 != parametersCount)
|
|
sql[i*2 + 1] = ',';
|
|
}
|
|
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.
|
|
pub fn computeRequiredSpaceForParametersNumbers(parametersCount: usize, alreadyUsedParameters: usize) usize {
|
|
var remainingUsedParameters = alreadyUsedParameters; // Initialize the count of used parameters to mark as taken.
|
|
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) {
|
|
if (currentSliceSize <= remainingUsedParameters) {
|
|
// All numbers of the current slice are taken by the already used parameters.
|
|
remainingUsedParameters -= currentSliceSize;
|
|
} else {
|
|
// Compute the count of numbers in the current slice.
|
|
const numbersCount = @min(remaining, currentSliceSize - remainingUsedParameters);
|
|
|
|
// Add the required string size of all numbers in this slice.
|
|
numbersSize += i * (numbersCount);
|
|
// Subtract the counted numbers in this current slice.
|
|
remaining -= numbersCount;
|
|
|
|
// No remaining used parameters.
|
|
remainingUsedParameters = 0;
|
|
}
|
|
|
|
// Move to the next slice.
|
|
i += 1;
|
|
currentSliceSize *= 10;
|
|
}
|
|
|
|
// Return the computed numbers size.
|
|
return numbersSize;
|
|
}
|
|
|
|
/// Copy the given source query and replace '?' parameters by numbered parameters.
|
|
pub fn copyAndReplaceSqlParameters(currentParameter: *usize, parametersCount: usize, writer: std.ArrayList(u8).Writer, source: []const u8) !void {
|
|
// If there are no parameters, just copy source SQL.
|
|
if (parametersCount <= 0) {
|
|
try writer.writeAll(source);
|
|
return;
|
|
}
|
|
|
|
for (source) |char| {
|
|
// Copy each character but '?', replaced by the current parameter string.
|
|
|
|
if (char == '?') {
|
|
// Copy the parameter string in place of '?'.
|
|
try writer.print("${d}", .{currentParameter.*});
|
|
// Increment parameter count.
|
|
currentParameter.* += 1;
|
|
} else {
|
|
// Simply write the current character.
|
|
try writer.writeByte(char);
|
|
}
|
|
}
|
|
}
|