Create ZRM, repositories and its related queries.
This commit is contained in:
commit
0eecb4df99
22 changed files with 2403 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
# IntelliJ IDEA
|
||||
*.iml
|
||||
.idea/
|
||||
|
||||
# Zig
|
||||
.zig-cache/
|
||||
zig-out/
|
9
LICENSE
Normal file
9
LICENSE
Normal file
|
@ -0,0 +1,9 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 Zeptotech
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
59
README.md
Normal file
59
README.md
Normal file
|
@ -0,0 +1,59 @@
|
|||
<p align="center">
|
||||
<a href="https://code.zeptotech.net/zedd/zrm">
|
||||
<picture>
|
||||
<img alt="ZRM logo" width="150" src="https://code.zeptotech.net/zedd/zrm/raw/branch/main/logo.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">
|
||||
ZRM
|
||||
</h1>
|
||||
|
||||
<h4 align="center">
|
||||
<a href="https://code.zeptotech.net/zedd/zrm">Documentation</a>
|
||||
|
|
||||
<a href="https://zedd.zeptotech.net/zrm/api">API</a>
|
||||
</h4>
|
||||
|
||||
<p align="center">
|
||||
Zig relational mapper
|
||||
</p>
|
||||
|
||||
ZRM is part of [_zedd_](https://code.zeptotech.net/zedd), a collection of useful libraries for zig.
|
||||
|
||||
## ZRM
|
||||
|
||||
_ZRM_ provides a simple interface to relational databases in Zig. Define your repositories and easily write queries to retrieve and save complex Zig structures.
|
||||
|
||||
## Versions
|
||||
|
||||
ZRM 0.1.0 is made and tested with zig 0.13.0.
|
||||
|
||||
## How to use
|
||||
|
||||
### Install
|
||||
|
||||
In your project directory:
|
||||
|
||||
```shell
|
||||
$ zig fetch --save https://code.zeptotech.net/zedd/zrm/archive/v0.1.0.tar.gz
|
||||
```
|
||||
|
||||
In `build.zig`:
|
||||
|
||||
```zig
|
||||
// Add zrm dependency.
|
||||
const zrm = b.dependency("zrm", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
exe.root_module.addImport("zrm", zrm.module("zrm"));
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
Full examples can be found in `tests` directory:
|
||||
|
||||
- [`tests/repository.zig`](https://code.zeptotech.net/zedd/zrm/src/branch/main/tests/repository.zig)
|
||||
- [`tests/composite.zig`](https://code.zeptotech.net/zedd/zrm/src/branch/main/tests/composite.zig)
|
95
build.zig
Normal file
95
build.zig
Normal file
|
@ -0,0 +1,95 @@
|
|||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
// Standard target options allows the person running `zig build` to choose
|
||||
// what target to build for. Here we do not override the defaults, which
|
||||
// means any target is allowed, and the default is native. Other options
|
||||
// for restricting supported target set are available.
|
||||
const target = b.standardTargetOptions(.{});
|
||||
|
||||
// Standard optimization options allow the person running `zig build` to select
|
||||
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
|
||||
// set a preferred release mode, allowing the user to decide how to optimize.
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
// Add zollections dependency.
|
||||
const zollections = b.dependency("zollections", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
// Add pg.zig dependency.
|
||||
const pg = b.dependency("pg", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const lib = b.addSharedLibrary(.{
|
||||
.name = "zrm",
|
||||
.root_source_file = b.path("src/root.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// This declares intent for the library to be installed into the standard
|
||||
// location when the user invokes the "install" step (the default step when
|
||||
// running `zig build`).
|
||||
b.installArtifact(lib);
|
||||
|
||||
// Add zrm module.
|
||||
const zrm_module = b.addModule("zrm", .{
|
||||
.root_source_file = b.path("src/root.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// Add dependencies.
|
||||
lib.root_module.addImport("zollections", zollections.module("zollections"));
|
||||
zrm_module.addImport("zollections", zollections.module("zollections"));
|
||||
lib.root_module.addImport("pg", pg.module("pg"));
|
||||
zrm_module.addImport("pg", pg.module("pg"));
|
||||
|
||||
// Creates a step for unit testing. This only builds the test executable
|
||||
// but does not run it.
|
||||
const lib_unit_tests = b.addTest(.{
|
||||
.root_source_file = b.path("tests/root.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// Add zouter dependency.
|
||||
lib_unit_tests.root_module.addImport("zrm", zrm_module);
|
||||
lib_unit_tests.root_module.addImport("zollections", zollections.module("zollections"));
|
||||
lib_unit_tests.root_module.addImport("pg", pg.module("pg"));
|
||||
|
||||
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
|
||||
run_lib_unit_tests.has_side_effects = true;
|
||||
|
||||
// Add an executable for tests global setup.
|
||||
const tests_setup = b.addExecutable(.{
|
||||
.name = "tests_setup",
|
||||
.root_source_file = b.path("tests/setup.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
tests_setup.root_module.addImport("pg", pg.module("pg"));
|
||||
const run_tests_setup = b.addRunArtifact(tests_setup);
|
||||
|
||||
// Similar to creating the run step earlier, this exposes a `test` step to
|
||||
// the `zig build --help` menu, providing a way for the user to request
|
||||
// running the unit tests.
|
||||
const test_step = b.step("test", "Run unit tests.");
|
||||
test_step.dependOn(&run_tests_setup.step);
|
||||
test_step.dependOn(&run_lib_unit_tests.step);
|
||||
|
||||
|
||||
// Documentation generation.
|
||||
const install_docs = b.addInstallDirectory(.{
|
||||
.source_dir = lib.getEmittedDocs(),
|
||||
.install_dir = .prefix,
|
||||
.install_subdir = "docs",
|
||||
});
|
||||
|
||||
// Documentation generation step.
|
||||
const docs_step = b.step("docs", "Emit documentation.");
|
||||
docs_step.dependOn(&install_docs.step);
|
||||
}
|
25
build.zig.zon
Normal file
25
build.zig.zon
Normal file
|
@ -0,0 +1,25 @@
|
|||
.{
|
||||
.name = "zrm",
|
||||
.version = "0.1.0",
|
||||
|
||||
.minimum_zig_version = "0.13.0",
|
||||
|
||||
.dependencies = .{
|
||||
.zollections = .{
|
||||
.url = "https://code.zeptotech.net/zedd/zollections/archive/v0.1.1.tar.gz",
|
||||
.hash = "12200fe147879d72381633e6f44d76db2c8a603cda1969b4e474c15c31052dbb24b7",
|
||||
},
|
||||
.pg = .{
|
||||
.url = "git+https://github.com/karlseguin/pg.zig?ref=zig-0.13#239a4468163a49d8c0d03285632eabe96003e9e2",
|
||||
.hash = "1220a1d7e51e2fa45e547c76a9e099c09d06e14b0b9bfc6baa89367f56f1ded399a0",
|
||||
},
|
||||
},
|
||||
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
},
|
||||
}
|
35
logo.svg
Normal file
35
logo.svg
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="512"
|
||||
height="512"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><g
|
||||
id="layer1"><g
|
||||
style="display:inline;fill:#000000;fill-opacity:1"
|
||||
id="g19"
|
||||
transform="matrix(2.0189327,0,0,2.0189327,249.8064,248.00901)"><path
|
||||
d="m 20,18 c 0,2.2091 -3.5817,4 -8,4 -4.41828,0 -8,-1.7909 -8,-4 v -4.026 c 0.50221,0.6166 1.21495,1.1289 2.00774,1.5252 C 7.58004,16.2854 9.69967,16.75 12,16.75 c 2.3003,0 4.42,-0.4646 5.9923,-1.2508 C 18.7851,15.1029 19.4978,14.5906 20,13.974 Z"
|
||||
fill="#1c274c"
|
||||
id="path17"
|
||||
style="fill:#000000;fill-opacity:1"
|
||||
transform="matrix(11.494213,0,0,11.494213,-134.80451,-133.9725)" /><path
|
||||
d="m 12,10.75 c 2.3003,0 4.42,-0.4646 5.9923,-1.25075 C 18.7851,9.10285 19.4978,8.59059 20,7.97397 V 12 c 0,0.5 -1.7857,1.5911 -2.6786,2.1576 C 15.9983,14.8192 14.118,15.25 12,15.25 9.88205,15.25 8.00168,14.8192 6.67856,14.1576 5.5,13.5683 4,12.5 4,12 V 7.97397 C 4.50221,8.59059 5.21495,9.10285 6.00774,9.49925 7.58004,10.2854 9.69967,10.75 12,10.75 Z"
|
||||
fill="#1c274c"
|
||||
id="path18"
|
||||
style="fill:#000000;fill-opacity:1"
|
||||
transform="matrix(11.494213,0,0,11.494213,-134.80451,-133.9725)" /><path
|
||||
d="M 17.3214,8.15761 C 15.9983,8.81917 14.118,9.25 12,9.25 9.88205,9.25 8.00168,8.81917 6.67856,8.15761 6.16384,7.95596 5.00637,7.31492 4.2015,6.27935 4.06454,6.10313 4.00576,5.87853 4.03988,5.65798 4.06283,5.50969 4.0948,5.35695 4.13578,5.26226 4.82815,3.40554 8.0858,2 12,2 c 3.9142,0 7.1718,1.40554 7.8642,3.26226 0.041,0.09469 0.073,0.24743 0.0959,0.39572 0.0341,0.22055 -0.0246,0.44515 -0.1616,0.62137 -0.8049,1.03557 -1.9623,1.67661 -2.4771,1.87826 z"
|
||||
fill="#1c274c"
|
||||
id="path19"
|
||||
style="fill:#000000;fill-opacity:1"
|
||||
transform="matrix(11.494213,0,0,11.494213,-134.80451,-133.9725)" /></g><path
|
||||
id="polygon3"
|
||||
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:23.5293"
|
||||
d="M 276.04706,168.09412 -10e-7,474.65883 150.16471,431.62355 180.35381,405.66114 512,37.341172 362.30588,80.258809 Z" /></g></svg>
|
After Width: | Height: | Size: 2.3 KiB |
181
src/conditions.zig
Normal file
181
src/conditions.zig
Normal file
|
@ -0,0 +1,181 @@
|
|||
const std = @import("std");
|
||||
const _sql = @import("sql.zig");
|
||||
const errors = @import("errors.zig");
|
||||
|
||||
const Static = @This();
|
||||
|
||||
/// 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 {
|
||||
// Initialize the SQL condition string.
|
||||
var comptimeSql: [_column.len + 1 + operator.len + 1 + 1]u8 = undefined;
|
||||
@memcpy(comptimeSql[0.._column.len], _column);
|
||||
@memcpy(comptimeSql[_column.len.._column.len + 1], " ");
|
||||
@memcpy(comptimeSql[_column.len + 1..(_column.len + 1 + operator.len)], operator);
|
||||
@memcpy(comptimeSql[_column.len + 1 + operator.len..], " ?");
|
||||
|
||||
// Initialize SQL buffer and set its value to comptime-generated SQL.
|
||||
const sqlBuf = try allocator.alloc(u8, comptimeSql.len);
|
||||
std.mem.copyForwards(u8, sqlBuf, &comptimeSql);
|
||||
|
||||
// Initialize parameters array.
|
||||
const params = try allocator.alloc(_sql.QueryParameter, 1);
|
||||
params[0] = try _sql.QueryParameter.fromValue(_value);
|
||||
|
||||
// Return the built SQL condition.
|
||||
return .{
|
||||
.sql = sqlBuf,
|
||||
.params = params,
|
||||
};
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
// Initialize the SQL condition string.
|
||||
var comptimeSql: [_column.len + 1 + operator.len + 1 + valueColumn.len]u8 = undefined;
|
||||
@memcpy(comptimeSql[0.._column.len], _column);
|
||||
@memcpy(comptimeSql[_column.len.._column.len + 1], " ");
|
||||
@memcpy(comptimeSql[_column.len + 1..(_column.len + 1 + operator.len)], operator);
|
||||
@memcpy(comptimeSql[_column.len + 1 + operator.len.._column.len + 1 + operator.len + 1], " ");
|
||||
@memcpy(comptimeSql[_column.len + 1 + operator.len + 1..], valueColumn);
|
||||
|
||||
// Initialize SQL buffer and set its value to comptime-generated SQL.
|
||||
const sqlBuf = try allocator.alloc(u8, comptimeSql.len);
|
||||
std.mem.copyForwards(u8, sqlBuf, &comptimeSql);
|
||||
|
||||
// Return the built SQL condition.
|
||||
return .{
|
||||
.sql = sqlBuf,
|
||||
.params = &[0]_sql.QueryParameter{},
|
||||
};
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
// Generate parameters SQL.
|
||||
const parametersSql = try _sql.generateParametersSql(allocator, _value.len);
|
||||
// Get all query parameters from given values.
|
||||
var valueParameters: []_sql.QueryParameter = try allocator.alloc(_sql.QueryParameter, _value.len);
|
||||
for (0.._value.len) |i| {
|
||||
// Convert every given value to a query parameter.
|
||||
valueParameters[i] = try _sql.QueryParameter.fromValue(_value[i]);
|
||||
}
|
||||
|
||||
// Initialize the SQL condition string.
|
||||
var sqlBuf: []u8 = try allocator.alloc(u8, _column.len + 1 + 2 + 1 + 1 + parametersSql.len + 1);
|
||||
std.mem.copyForwards(u8, sqlBuf[0.._column.len], _column);
|
||||
std.mem.copyForwards(u8, sqlBuf[_column.len.._column.len + 1 + 2 + 1 + 1], " IN (");
|
||||
std.mem.copyForwards(u8, sqlBuf[_column.len + 1 + 2 + 1 + 1.._column.len + 1 + 2 + 1 + 1 + parametersSql.len], parametersSql);
|
||||
std.mem.copyForwards(u8, sqlBuf[_column.len + 1 + 2 + 1 + 1 + parametersSql.len..], ")");
|
||||
|
||||
// Return the built SQL condition.
|
||||
return .{
|
||||
.sql = sqlBuf,
|
||||
.params = valueParameters,
|
||||
};
|
||||
}
|
||||
|
||||
/// Generic conditions combiner generator.
|
||||
fn conditionsCombiner(comptime keyword: []const u8, allocator: std.mem.Allocator, subconditions: []const _sql.SqlParams) !_sql.SqlParams {
|
||||
if (subconditions.len == 0) {
|
||||
// At least one condition is required.
|
||||
return errors.ZrmError.AtLeastOneConditionRequired;
|
||||
}
|
||||
|
||||
// Full keyword constant.
|
||||
const fullKeyword = " " ++ keyword ++ " ";
|
||||
|
||||
// Compute size of the SQL to generate, and the count of query parameters in total.
|
||||
var sqlSize: usize = 1 + 1; // parentheses.
|
||||
var queryParametersCount: usize = 0;
|
||||
for (subconditions) |subcondition| {
|
||||
sqlSize += subcondition.sql.len;
|
||||
queryParametersCount += subcondition.params.len;
|
||||
}
|
||||
// There are n-1 keywords.
|
||||
sqlSize += (subconditions.len - 1) * fullKeyword.len;
|
||||
|
||||
// Initialize the SQL condition string.
|
||||
var sqlBuf = try allocator.alloc(u8, sqlSize);
|
||||
// Initialize the query parameters array.
|
||||
var parameters = try allocator.alloc(_sql.QueryParameter, queryParametersCount);
|
||||
var sqlBufCursor: usize = 0; var parametersCursor: usize = 0;
|
||||
|
||||
// Add first parenthesis.
|
||||
sqlBuf[sqlBufCursor] = '('; sqlBufCursor += 1;
|
||||
|
||||
// Add all subconditions.
|
||||
for (0..subconditions.len) |i| {
|
||||
// Add each subcondition to SQL.
|
||||
const subcondition = subconditions[i];
|
||||
std.mem.copyForwards(u8, sqlBuf[sqlBufCursor..sqlBufCursor + subcondition.sql.len], subcondition.sql);
|
||||
sqlBufCursor += subcondition.sql.len;
|
||||
|
||||
if (i < subconditions.len - 1) {
|
||||
// Append the keyword, if required.
|
||||
@memcpy(sqlBuf[sqlBufCursor..sqlBufCursor + fullKeyword.len], fullKeyword);
|
||||
sqlBufCursor += fullKeyword.len;
|
||||
}
|
||||
|
||||
// Add query parameters to the array.
|
||||
std.mem.copyForwards(_sql.QueryParameter, parameters[parametersCursor..parametersCursor+subcondition.params.len], subcondition.params);
|
||||
parametersCursor += subcondition.params.len;
|
||||
}
|
||||
|
||||
// Add last parenthesis.
|
||||
sqlBuf[sqlBufCursor] = ')'; sqlBufCursor += 1;
|
||||
|
||||
// Return built SQL params.
|
||||
return .{
|
||||
.sql = sqlBuf,
|
||||
.params = parameters,
|
||||
};
|
||||
}
|
||||
|
||||
/// Create an AND condition between multiple sub-conditions.
|
||||
pub fn @"and"(allocator: std.mem.Allocator, subconditions: []const _sql.SqlParams) !_sql.SqlParams {
|
||||
return conditionsCombiner("AND", allocator, subconditions);
|
||||
}
|
||||
|
||||
/// Create an OR condition between multiple sub-conditions.
|
||||
pub fn @"or"(allocator: std.mem.Allocator, subconditions: []const _sql.SqlParams) !_sql.SqlParams {
|
||||
return conditionsCombiner("OR", allocator, subconditions);
|
||||
}
|
||||
|
||||
/// A conditions builder.
|
||||
pub const Builder = struct {
|
||||
const Self = @This();
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
/// 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 {
|
||||
return Static.value(ValueType, self.allocator, _column, operator, _value);
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
return Static.column(self.allocator, _column, operator, valueColumn);
|
||||
}
|
||||
|
||||
/// Create an IN condition on a column.
|
||||
pub fn in(self: Self, comptime ValueType: type, _column: []const u8, _value: []const ValueType) !_sql.SqlParams {
|
||||
return Static.in(ValueType, self.allocator, _column, _value);
|
||||
}
|
||||
|
||||
/// Create an AND condition between multiple sub-conditions.
|
||||
pub fn @"and"(self: Self, subconditions: []const _sql.SqlParams) !_sql.SqlParams {
|
||||
return Static.@"and"(self.allocator, subconditions);
|
||||
}
|
||||
|
||||
/// Create an OR condition between multiple sub-conditions.
|
||||
pub fn @"or"(self: Self, subconditions: []const _sql.SqlParams) !_sql.SqlParams {
|
||||
return Static.@"or"(self.allocator, subconditions);
|
||||
}
|
||||
|
||||
/// Initialize a new conditions builder with the given allocator.
|
||||
pub fn init(allocator: std.mem.Allocator) Self {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
};
|
10
src/errors.zig
Normal file
10
src/errors.zig
Normal file
|
@ -0,0 +1,10 @@
|
|||
const std = @import("std");
|
||||
|
||||
pub const ZrmError = error {
|
||||
QueryFailed,
|
||||
UnsupportedTableType,
|
||||
AtLeastOneValueRequired,
|
||||
AtLeastOneConditionRequired,
|
||||
AtLeastOneSelectionRequired,
|
||||
UpdatedValuesRequired,
|
||||
};
|
9
src/global.zig
Normal file
9
src/global.zig
Normal file
|
@ -0,0 +1,9 @@
|
|||
const std = @import("std");
|
||||
|
||||
/// Set if debug mode is enabled or not.
|
||||
pub var debugMode: bool = false;
|
||||
|
||||
/// Set debug mode status.
|
||||
pub fn setDebug(comptime debug: bool) void {
|
||||
debugMode = debug;
|
||||
}
|
346
src/insert.zig
Normal file
346
src/insert.zig
Normal file
|
@ -0,0 +1,346 @@
|
|||
const std = @import("std");
|
||||
const pg = @import("pg");
|
||||
const zollections = @import("zollections");
|
||||
const errors = @import("errors.zig");
|
||||
const postgresql = @import("postgresql.zig");
|
||||
const _sql = @import("sql.zig");
|
||||
const repository = @import("repository.zig");
|
||||
|
||||
/// Type of an insertable column. Insert shape should be composed of only these.
|
||||
pub fn Insertable(comptime ValueType: type) type {
|
||||
return struct {
|
||||
value: ?ValueType = null,
|
||||
default: bool = false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Repository insert query configuration structure.
|
||||
pub fn RepositoryInsertConfiguration(comptime InsertShape: type) type {
|
||||
return struct {
|
||||
values: []const InsertShape = undefined,
|
||||
returning: ?_sql.SqlParams = null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Repository models insert manager.
|
||||
/// Manage insert query string build and execution.
|
||||
pub fn RepositoryInsert(comptime Model: type, comptime TableShape: type, comptime repositoryConfig: repository.RepositoryConfiguration(Model, TableShape), comptime InsertShape: type) type {
|
||||
// Create columns list.
|
||||
const columns = comptime columnsFinder: {
|
||||
// Get insert shape type data.
|
||||
const insertType = @typeInfo(InsertShape);
|
||||
// Initialize a columns slice of "fields len" size.
|
||||
var columnsList: [insertType.Struct.fields.len][]const u8 = undefined;
|
||||
|
||||
// Add structure fields to the columns slice.
|
||||
var i: usize = 0;
|
||||
for (insertType.Struct.fields) |field| {
|
||||
// Check that the table type defines the same fields.
|
||||
if (!@hasField(TableShape, field.name))
|
||||
//TODO check its type?
|
||||
@compileError("The table doesn't contain the indicated insert columns.");
|
||||
|
||||
// Add each structure field to columns list.
|
||||
columnsList[i] = field.name;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// Assign built columns list.
|
||||
break :columnsFinder columnsList;
|
||||
};
|
||||
|
||||
// Pre-compute SQL buffer size.
|
||||
const sqlBase = "INSERT INTO " ++ repositoryConfig.table ++ comptime buildInsertColumns: {
|
||||
// Compute the size of the insert columns buffer.
|
||||
var insertColumnsSize = 0;
|
||||
for (columns) |column| {
|
||||
insertColumnsSize += column.len + 1;
|
||||
}
|
||||
insertColumnsSize = insertColumnsSize - 1 + 2; // 2 for parentheses.
|
||||
|
||||
var columnsBuf: [insertColumnsSize]u8 = undefined;
|
||||
// Initialize columns buffer cursor.
|
||||
var columnsBufCursor = 0;
|
||||
// Open parentheses.
|
||||
columnsBuf[columnsBufCursor] = '('; columnsBufCursor += 1;
|
||||
|
||||
for (columns) |column| {
|
||||
// Write each column name, with a ',' as separator.
|
||||
@memcpy(columnsBuf[columnsBufCursor..columnsBufCursor+column.len+1], column ++ ",");
|
||||
columnsBufCursor += column.len + 1;
|
||||
}
|
||||
|
||||
// Replace the last ',' by a ')'.
|
||||
columnsBuf[columnsBufCursor - 1] = ')';
|
||||
|
||||
break :buildInsertColumns columnsBuf;
|
||||
} ++ " VALUES ";
|
||||
|
||||
// Initialize the RETURNING clause.
|
||||
const returningClause = "RETURNING";
|
||||
|
||||
// INSERT INTO {repositoryConfig.table} VALUES ?;
|
||||
const fixedSqlSize = sqlBase.len + 0 + 1;
|
||||
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
const Configuration = RepositoryInsertConfiguration(InsertShape);
|
||||
|
||||
arena: std.heap.ArenaAllocator,
|
||||
database: *pg.Pool,
|
||||
insertConfig: Configuration,
|
||||
|
||||
sql: ?[]const u8 = null,
|
||||
|
||||
/// Parse given model or shape and put the result in newValue.
|
||||
fn parseData(newValue: *InsertShape, value: anytype) !void {
|
||||
// If the given value is a model, first convert it to its SQL equivalent.
|
||||
if (@TypeOf(value) == Model) {
|
||||
return parseData(newValue, try repositoryConfig.toSql(value));
|
||||
}
|
||||
|
||||
inline for (columns) |column| {
|
||||
@field(newValue.*, column) = .{ .value = @field(value, column) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse one value to insert.
|
||||
fn parseOne(self: *Self, value: anytype) !void {
|
||||
const newValues = try self.arena.allocator().alloc(InsertShape, 1);
|
||||
// Parse the given value.
|
||||
try parseData(&newValues[0], value);
|
||||
self.insertConfig.values = newValues;
|
||||
}
|
||||
|
||||
/// Parse a slice of values to insert.
|
||||
fn parseSlice(self: *Self, value: anytype) !void {
|
||||
const newValues = try self.arena.allocator().alloc(InsertShape, value.len);
|
||||
for (0..value.len) |i| {
|
||||
// Parse each value in the given slice.
|
||||
try parseData(&newValues[i], value[i]);
|
||||
}
|
||||
self.insertConfig.values = newValues;
|
||||
}
|
||||
|
||||
/// Set values to insert.
|
||||
/// Values can be Model, TableShape or InsertShape.
|
||||
pub fn values(self: *Self, _values: anytype) !void {
|
||||
// Get values type.
|
||||
const valuesType = @TypeOf(_values);
|
||||
|
||||
switch (@typeInfo(valuesType)) {
|
||||
.Pointer => |ptr| {
|
||||
switch (ptr.size) {
|
||||
// It's a single object.
|
||||
.One => switch (@typeInfo(ptr.child)) {
|
||||
// It's an array, parse it.
|
||||
.Array => try self.parseSlice(_values),
|
||||
// It's a structure, parse it.
|
||||
.Struct => try self.parseOne(_values.*),
|
||||
else => @compileError("Cannot insert values of type " ++ @typeName(ptr.child)),
|
||||
},
|
||||
// It's a slice, parse it.
|
||||
else => switch (@typeInfo(ptr.child)) {
|
||||
.Struct => try self.parseSlice(_values),
|
||||
else => @compileError("Cannot insert values of type " ++ @typeName(ptr.child)),
|
||||
}
|
||||
}
|
||||
},
|
||||
// It's a structure, just parse it.
|
||||
.Struct => try self.parseOne(_values),
|
||||
|
||||
else => @compileError("Cannot insert values of type " ++ @typeName(valuesType)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set selected columns for RETURNING clause.
|
||||
pub fn returning(self: *Self, _select: _sql.SqlParams) void {
|
||||
self.insertConfig.returning = _select;
|
||||
}
|
||||
|
||||
/// Set selected columns for RETURNING clause.
|
||||
pub fn returningColumns(self: *Self, _select: []const []const u8) void {
|
||||
if (_select.len == 0) {
|
||||
return errors.AtLeastOneSelectionRequired;
|
||||
}
|
||||
|
||||
self.returning(.{
|
||||
// Join selected columns.
|
||||
.sql = std.mem.join(self.arena.allocator(), ", ", _select),
|
||||
.params = &[_]_sql.QueryParameter{}, // No parameters.
|
||||
});
|
||||
}
|
||||
|
||||
/// Set RETURNING all columns of the table after insert.
|
||||
pub fn returningAll(self: *Self) void {
|
||||
self.returning(.{
|
||||
.sql = "*",
|
||||
.params = &[_]_sql.QueryParameter{}, // No parameters.
|
||||
});
|
||||
}
|
||||
|
||||
/// Build SQL query.
|
||||
pub fn buildSql(self: *Self) !void {
|
||||
if (self.insertConfig.values.len == 0) {
|
||||
// At least one value is required to insert.
|
||||
return errors.ZrmError.AtLeastOneValueRequired;
|
||||
}
|
||||
|
||||
// Compute VALUES parameters count.
|
||||
const valuesParametersCount = self.insertConfig.values.len * columns.len;
|
||||
|
||||
// Compute values SQL size (format: "($1,$2,$3),($4,$5,$6),($7,$8,$9)").
|
||||
const valuesSqlSize = _sql.computeRequiredSpaceForParametersNumbers(valuesParametersCount, 0)
|
||||
+ valuesParametersCount // Dollars in values sets.
|
||||
+ (self.insertConfig.values.len * (columns.len - 1)) // ',' separators in values sets.
|
||||
+ (self.insertConfig.values.len - 1) // ',' separators between values sets.
|
||||
+ (self.insertConfig.values.len * 2) // Parentheses of values sets.
|
||||
;
|
||||
|
||||
// Compute RETURNING size.
|
||||
const returningSize: usize = if (self.insertConfig.returning) |_returning| (
|
||||
1 + returningClause.len + _returning.sql.len + 1 + _sql.computeRequiredSpaceForParametersNumbers(_returning.params.len, valuesParametersCount)
|
||||
) else 0;
|
||||
|
||||
// Initialize SQL buffer.
|
||||
const sqlBuf = try self.arena.allocator().alloc(u8, fixedSqlSize + valuesSqlSize + returningSize);
|
||||
|
||||
// Append initial "INSERT INTO table VALUES ".
|
||||
@memcpy(sqlBuf[0..sqlBase.len],sqlBase);
|
||||
var sqlBufCursor: usize = sqlBase.len;
|
||||
|
||||
// Start parameter counter at 1.
|
||||
var currentParameter: usize = 1;
|
||||
|
||||
if (self.insertConfig.values.len == 0) {
|
||||
// No values, output an empty values set.
|
||||
std.mem.copyForwards(u8, sqlBuf[sqlBufCursor..sqlBufCursor+2], "()");
|
||||
sqlBufCursor += 2;
|
||||
} else {
|
||||
// Build values set.
|
||||
for (self.insertConfig.values) |_| {
|
||||
// Add the first '('.
|
||||
sqlBuf[sqlBufCursor] = '('; sqlBufCursor += 1;
|
||||
inline for (columns) |_| {
|
||||
// Create the parameter string and append it to the SQL buffer.
|
||||
const paramSize = 1 + try _sql.computeRequiredSpaceForParameter(currentParameter) + 1;
|
||||
_ = try std.fmt.bufPrint(sqlBuf[sqlBufCursor..sqlBufCursor+paramSize], "${d},", .{currentParameter});
|
||||
sqlBufCursor += paramSize;
|
||||
// Increment parameter count.
|
||||
currentParameter += 1;
|
||||
}
|
||||
// Replace the final ',' with a ')'.
|
||||
sqlBuf[sqlBufCursor - 1] = ')';
|
||||
// Add the final ','.
|
||||
sqlBuf[sqlBufCursor] = ','; sqlBufCursor += 1;
|
||||
}
|
||||
sqlBufCursor -= 1;
|
||||
}
|
||||
|
||||
// Append RETURNING clause, if there is one defined.
|
||||
if (self.insertConfig.returning) |_returning| {
|
||||
@memcpy(sqlBuf[sqlBufCursor..sqlBufCursor+(1 + returningClause.len + 1)], " " ++ returningClause ++ " ");
|
||||
// Copy RETURNING clause content and replace parameters, if there are some.
|
||||
try _sql.copyAndReplaceSqlParameters(¤tParameter,
|
||||
_returning.params.len,
|
||||
sqlBuf[sqlBufCursor+(1+returningClause.len+1)..sqlBufCursor+returningSize], _returning.sql
|
||||
);
|
||||
sqlBufCursor += returningSize;
|
||||
}
|
||||
|
||||
// ";" to end the query.
|
||||
sqlBuf[sqlBufCursor] = ';'; sqlBufCursor += 1;
|
||||
|
||||
// Save built SQL query.
|
||||
self.sql = sqlBuf;
|
||||
}
|
||||
|
||||
/// Execute the insert query.
|
||||
fn execQuery(self: *Self) !*pg.Result {
|
||||
// Get a connection to the database.
|
||||
const connection = try self.database.acquire();
|
||||
errdefer connection.release();
|
||||
|
||||
// Initialize a new PostgreSQL statement.
|
||||
var statement = try pg.Stmt.init(connection, .{
|
||||
.column_names = true,
|
||||
.release_conn = true,
|
||||
.allocator = self.arena.allocator(),
|
||||
});
|
||||
errdefer statement.deinit();
|
||||
|
||||
// Prepare SQL insert query.
|
||||
statement.prepare(self.sql.?)
|
||||
catch |err| return postgresql.handlePostgresqlError(err, connection, &statement);
|
||||
|
||||
// Bind INSERT query parameters.
|
||||
for (self.insertConfig.values) |row| {
|
||||
inline for (columns) |column| {
|
||||
try statement.bind(@field(row, column).value);
|
||||
}
|
||||
}
|
||||
// Bind RETURNING query parameters.
|
||||
if (self.insertConfig.returning) |_returning| {
|
||||
try postgresql.bindQueryParameters(&statement, _returning.params);
|
||||
}
|
||||
|
||||
// Execute the query and get its result.
|
||||
const result = statement.execute()
|
||||
catch |err| return postgresql.handlePostgresqlError(err, connection, &statement);
|
||||
|
||||
// Query executed successfully, return the result.
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Insert given models.
|
||||
pub fn insert(self: *Self, allocator: std.mem.Allocator) !repository.RepositoryResult(Model) {
|
||||
// Build SQL query if it wasn't built.
|
||||
if (self.sql) |_| {} else { try self.buildSql(); }
|
||||
|
||||
// Execute query and get its result.
|
||||
const queryResult = try self.execQuery();
|
||||
|
||||
//TODO deduplicate this in postgresql.zig, we could do it if Mapper type was exposed.
|
||||
//TODO make a generic mapper and do it in repository.zig?
|
||||
// Create an arena for mapper data.
|
||||
var mapperArena = std.heap.ArenaAllocator.init(allocator);
|
||||
// Get result mapper.
|
||||
const mapper = queryResult.mapper(TableShape, .{ .allocator = mapperArena.allocator() });
|
||||
|
||||
// Initialize models list.
|
||||
var models = std.ArrayList(*Model).init(allocator);
|
||||
defer models.deinit();
|
||||
|
||||
// Get all raw models from the result mapper.
|
||||
while (try mapper.next()) |rawModel| {
|
||||
// Parse each raw model from the mapper.
|
||||
const model = try allocator.create(Model);
|
||||
model.* = try repositoryConfig.fromSql(rawModel);
|
||||
try models.append(model);
|
||||
}
|
||||
|
||||
// Return a result with the models.
|
||||
return repository.RepositoryResult(Model).init(allocator,
|
||||
zollections.Collection(Model).init(allocator, try models.toOwnedSlice()),
|
||||
mapperArena,
|
||||
);
|
||||
}
|
||||
|
||||
/// Initialize a new repository insert query.
|
||||
pub fn init(allocator: std.mem.Allocator, database: *pg.Pool) Self {
|
||||
return .{
|
||||
// Initialize an arena allocator for the insert query.
|
||||
.arena = std.heap.ArenaAllocator.init(allocator),
|
||||
.database = database,
|
||||
.insertConfig = .{},
|
||||
};
|
||||
}
|
||||
|
||||
/// Deinitialize the repository insert query.
|
||||
pub fn deinit(self: *Self) void {
|
||||
// Free everything allocated for this insert query.
|
||||
self.arena.deinit();
|
||||
}
|
||||
};
|
||||
}
|
51
src/postgresql.zig
Normal file
51
src/postgresql.zig
Normal file
|
@ -0,0 +1,51 @@
|
|||
const std = @import("std");
|
||||
const pg = @import("pg");
|
||||
const global = @import("global.zig");
|
||||
const errors = @import("errors.zig");
|
||||
const _sql = @import("sql.zig");
|
||||
const repository = @import("repository.zig");
|
||||
|
||||
/// PostgreSQL query error details.
|
||||
pub const PostgresqlError = struct {
|
||||
code: []const u8,
|
||||
message: []const u8,
|
||||
};
|
||||
|
||||
/// Try to bind query parameters to the statement.
|
||||
pub fn bindQueryParameters(statement: *pg.Stmt, parameters: []const _sql.QueryParameter) !void {
|
||||
for (parameters) |parameter| {
|
||||
// Try to bind each parameter in the slice.
|
||||
try bindQueryParameter(statement, parameter);
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to bind a query parameter to the statement.
|
||||
pub fn bindQueryParameter(statement: *pg.Stmt, parameter: _sql.QueryParameter) !void {
|
||||
switch (parameter) {
|
||||
.integer => |integer| try statement.bind(integer),
|
||||
.number => |number| try statement.bind(number),
|
||||
.string => |string| try statement.bind(string),
|
||||
.bool => |boolVal| try statement.bind(boolVal),
|
||||
.null => try statement.bind(null),
|
||||
}
|
||||
}
|
||||
|
||||
/// PostgreSQL error handling by ZRM.
|
||||
pub fn handlePostgresqlError(err: anyerror, connection: *pg.Conn, statement: *pg.Stmt) anyerror {
|
||||
// Release connection and statement as query failed.
|
||||
defer statement.deinit();
|
||||
defer connection.release();
|
||||
|
||||
if (connection.err) |sqlErr| {
|
||||
if (global.debugMode) {
|
||||
// If debug mode is enabled, show the PostgreSQL error.
|
||||
std.debug.print("PostgreSQL error\n{s}: {s}\n", .{sqlErr.code, sqlErr.message});
|
||||
}
|
||||
|
||||
// Return that an error happened in query execution.
|
||||
return errors.ZrmError.QueryFailed;
|
||||
} else {
|
||||
// Not an SQL error, just return it.
|
||||
return err;
|
||||
}
|
||||
}
|
262
src/query.zig
Normal file
262
src/query.zig
Normal file
|
@ -0,0 +1,262 @@
|
|||
const std = @import("std");
|
||||
const pg = @import("pg");
|
||||
const zollections = @import("zollections");
|
||||
const errors = @import("errors.zig");
|
||||
const postgresql = @import("postgresql.zig");
|
||||
const _sql = @import("sql.zig");
|
||||
const conditions = @import("conditions.zig");
|
||||
const repository = @import("repository.zig");
|
||||
|
||||
/// Repository query configuration structure.
|
||||
pub const RepositoryQueryConfiguration = struct {
|
||||
select: ?_sql.SqlParams = null,
|
||||
join: ?_sql.SqlParams = null,
|
||||
where: ?_sql.SqlParams = null,
|
||||
};
|
||||
|
||||
/// Repository models query manager.
|
||||
/// Manage query string build and its execution.
|
||||
pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime repositoryConfig: repository.RepositoryConfiguration(Model, TableShape)) type {
|
||||
// Pre-compute SQL buffer size.
|
||||
const selectClause = "SELECT";
|
||||
const fromClause = "FROM";
|
||||
const whereClause = "WHERE";
|
||||
// SELECT ? FROM {repositoryConfig.table}??;
|
||||
const fixedSqlSize = selectClause.len + 1 + 0 + 1 + fromClause.len + 1 + repositoryConfig.table.len + 0 + 0 + 1;
|
||||
const defaultSelectSql = "*";
|
||||
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
arena: std.heap.ArenaAllocator,
|
||||
database: *pg.Pool,
|
||||
queryConfig: RepositoryQueryConfiguration,
|
||||
|
||||
sql: ?[]const u8 = null,
|
||||
|
||||
/// Set selected columns.
|
||||
pub fn select(self: *Self, _select: _sql.SqlParams) void {
|
||||
self.queryConfig.select = _select;
|
||||
}
|
||||
|
||||
/// Set selected columns for SELECT clause.
|
||||
pub fn selectColumns(self: *Self, _select: []const []const u8) !void {
|
||||
if (_select.len == 0) {
|
||||
return errors.AtLeastOneSelectionRequired;
|
||||
}
|
||||
|
||||
self.select(.{
|
||||
// Join selected columns.
|
||||
.sql = std.mem.join(self.arena.allocator(), ", ", _select),
|
||||
.params = &[_]_sql.QueryParameter{}, // No parameters.
|
||||
});
|
||||
}
|
||||
|
||||
/// Set JOIN clause.
|
||||
pub fn join(self: *Self, _join: _sql.SqlParams) void {
|
||||
self.queryConfig.join = _join;
|
||||
}
|
||||
|
||||
/// Set WHERE conditions.
|
||||
pub fn where(self: *Self, _where: _sql.SqlParams) void {
|
||||
self.queryConfig.where = _where;
|
||||
}
|
||||
|
||||
/// Create a new condition builder.
|
||||
pub fn newCondition(self: *Self) conditions.Builder {
|
||||
return conditions.Builder.init(self.arena.allocator());
|
||||
}
|
||||
|
||||
/// Set a WHERE value condition.
|
||||
pub fn whereValue(self: *Self, comptime ValueType: type, comptime _column: []const u8, comptime operator: []const u8, _value: ValueType) !void {
|
||||
self.where(
|
||||
try conditions.value(ValueType, self.arena.allocator(), _column, operator, _value)
|
||||
);
|
||||
}
|
||||
|
||||
/// Set a WHERE column condition.
|
||||
pub fn whereColumn(self: *Self, comptime _column: []const u8, comptime operator: []const u8, comptime _valueColumn: []const u8) !void {
|
||||
self.where(
|
||||
try conditions.column(self.arena.allocator(), _column, operator, _valueColumn)
|
||||
);
|
||||
}
|
||||
|
||||
/// Set a WHERE IN condition.
|
||||
pub fn whereIn(self: *Self, comptime ValueType: type, comptime _column: []const u8, _value: []const ValueType) !void {
|
||||
self.where(
|
||||
try conditions.in(ValueType, self.arena.allocator(), _column, _value)
|
||||
);
|
||||
}
|
||||
|
||||
/// Build SQL query.
|
||||
pub fn buildSql(self: *Self) !void {
|
||||
// Start parameter counter at 1.
|
||||
var currentParameter: usize = 1;
|
||||
|
||||
// Compute SELECT size.
|
||||
var selectSize: usize = defaultSelectSql.len;
|
||||
if (self.queryConfig.select) |_select| {
|
||||
selectSize = _select.sql.len + _sql.computeRequiredSpaceForParametersNumbers(_select.params.len, currentParameter - 1);
|
||||
currentParameter += _select.params.len;
|
||||
}
|
||||
|
||||
// Compute JOIN size.
|
||||
var joinSize: usize = 0;
|
||||
if (self.queryConfig.join) |_join| {
|
||||
joinSize = 1 + _join.sql.len + _sql.computeRequiredSpaceForParametersNumbers(_join.params.len, currentParameter - 1);
|
||||
currentParameter += _join.params.len;
|
||||
}
|
||||
|
||||
// Compute WHERE size.
|
||||
var whereSize: usize = 0;
|
||||
if (self.queryConfig.where) |_where| {
|
||||
whereSize = 1 + whereClause.len + _where.sql.len + 1 + _sql.computeRequiredSpaceForParametersNumbers(_where.params.len, currentParameter - 1);
|
||||
currentParameter += _where.params.len;
|
||||
}
|
||||
|
||||
// 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.
|
||||
self.sql = sqlBuf;
|
||||
}
|
||||
|
||||
/// Execute the built query.
|
||||
fn execQuery(self: *Self) !*pg.Result
|
||||
{
|
||||
// Get a connection to the database.
|
||||
const connection = try self.database.acquire();
|
||||
errdefer connection.release();
|
||||
|
||||
// Initialize a new PostgreSQL statement.
|
||||
var statement = try pg.Stmt.init(connection, .{
|
||||
.column_names = true,
|
||||
.release_conn = true,
|
||||
.allocator = self.arena.allocator(),
|
||||
});
|
||||
errdefer statement.deinit();
|
||||
|
||||
// Prepare SQL query.
|
||||
statement.prepare(self.sql.?)
|
||||
catch |err| return postgresql.handlePostgresqlError(err, connection, &statement);
|
||||
|
||||
// Bind query parameters.
|
||||
if (self.queryConfig.select) |_select|
|
||||
try postgresql.bindQueryParameters(&statement, _select.params);
|
||||
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.
|
||||
const result = statement.execute()
|
||||
catch |err| return postgresql.handlePostgresqlError(err, connection, &statement);
|
||||
|
||||
// Query executed successfully, return the result.
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Retrieve queried models.
|
||||
pub fn get(self: *Self, allocator: std.mem.Allocator) !repository.RepositoryResult(Model) {
|
||||
// Build SQL query if it wasn't built.
|
||||
if (self.sql) |_| {} else { try self.buildSql(); }
|
||||
|
||||
// Execute query and get its result.
|
||||
const queryResult = try self.execQuery();
|
||||
|
||||
//TODO deduplicate this in postgresql.zig, we could do it if Mapper type was exposed.
|
||||
//TODO make a generic mapper and do it in repository.zig?
|
||||
// Create an arena for mapper data.
|
||||
var mapperArena = std.heap.ArenaAllocator.init(allocator);
|
||||
// Get result mapper.
|
||||
const mapper = queryResult.mapper(TableShape, .{ .allocator = mapperArena.allocator() });
|
||||
|
||||
// Initialize models list.
|
||||
var models = std.ArrayList(*Model).init(allocator);
|
||||
defer models.deinit();
|
||||
|
||||
// Get all raw models from the result mapper.
|
||||
while (try mapper.next()) |rawModel| {
|
||||
// Parse each raw model from the mapper.
|
||||
const model = try allocator.create(Model);
|
||||
model.* = try repositoryConfig.fromSql(rawModel);
|
||||
try models.append(model);
|
||||
}
|
||||
|
||||
// Return a result with the models.
|
||||
return repository.RepositoryResult(Model).init(allocator,
|
||||
zollections.Collection(Model).init(allocator, try models.toOwnedSlice()),
|
||||
mapperArena,
|
||||
);
|
||||
}
|
||||
|
||||
/// Initialize a new repository query.
|
||||
pub fn init(allocator: std.mem.Allocator, database: *pg.Pool, queryConfig: RepositoryQueryConfiguration) Self {
|
||||
return .{
|
||||
// Initialize the query arena allocator.
|
||||
.arena = std.heap.ArenaAllocator.init(allocator),
|
||||
.database = database,
|
||||
.queryConfig = queryConfig,
|
||||
};
|
||||
}
|
||||
|
||||
/// Deinitialize the repository query.
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.arena.deinit();
|
||||
}
|
||||
};
|
||||
}
|
181
src/repository.zig
Normal file
181
src/repository.zig
Normal file
|
@ -0,0 +1,181 @@
|
|||
const std = @import("std");
|
||||
const pg = @import("pg");
|
||||
const zollections = @import("zollections");
|
||||
const _sql = @import("sql.zig");
|
||||
const query = @import("query.zig");
|
||||
const insert = @import("insert.zig");
|
||||
const update = @import("update.zig");
|
||||
|
||||
// Type of the "model from SQL data" function.
|
||||
pub fn ModelFromSql(comptime Model: type, comptime TableShape: type) type {
|
||||
return *const fn (raw: TableShape) anyerror!Model;
|
||||
}
|
||||
// Type of the "model to SQL data" function.
|
||||
pub fn ModelToSql(comptime Model: type, comptime TableShape: type) type {
|
||||
return *const fn (model: Model) anyerror!TableShape;
|
||||
}
|
||||
|
||||
/// Repository configuration structure.
|
||||
pub fn RepositoryConfiguration(comptime Model: type, comptime TableShape: type) type {
|
||||
return struct {
|
||||
/// Table name for this repository.
|
||||
table: []const u8,
|
||||
|
||||
/// Insert shape used by default for inserts in the repository.
|
||||
insertShape: type,
|
||||
|
||||
/// Key(s) of the model.
|
||||
key: []const []const u8,
|
||||
|
||||
/// Convert a model to an SQL table row.
|
||||
fromSql: ModelFromSql(Model, TableShape),
|
||||
/// Convert an SQL table row to a model.
|
||||
toSql: ModelToSql(Model, TableShape),
|
||||
};
|
||||
}
|
||||
|
||||
/// Repository of structures of a certain type.
|
||||
pub fn Repository(comptime Model: type, comptime TableShape: type, comptime config: RepositoryConfiguration(Model, TableShape)) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
pub const Query: type = query.RepositoryQuery(Model, TableShape, config);
|
||||
pub const Insert: type = insert.RepositoryInsert(Model, TableShape, config, config.insertShape);
|
||||
|
||||
pub fn InsertCustom(comptime InsertShape: type) type {
|
||||
return insert.RepositoryInsert(Model, TableShape, config, InsertShape);
|
||||
}
|
||||
|
||||
pub fn Update(comptime UpdateShape: type) type {
|
||||
return update.RepositoryUpdate(Model, TableShape, config, UpdateShape);
|
||||
}
|
||||
|
||||
/// Try to find the requested model.
|
||||
pub fn find(allocator: std.mem.Allocator, database: *pg.Pool, modelKey: anytype) !RepositoryResult(Model) {
|
||||
// Initialize a new query.
|
||||
var modelQuery = Self.Query.init(allocator, database, .{});
|
||||
defer modelQuery.deinit();
|
||||
|
||||
if (config.key.len == 1) {
|
||||
// Add a simple condition.
|
||||
try modelQuery.whereValue(std.meta.fields(TableShape)[std.meta.fieldIndex(TableShape, config.key[0]).?].type, config.key[0], "=", modelKey);
|
||||
} else {
|
||||
// Add conditions for all keys in the composite key.
|
||||
var conditions: [config.key.len]_sql.SqlParams = undefined;
|
||||
|
||||
inline for (config.key, &conditions) |keyName, *condition| {
|
||||
if (std.meta.fieldIndex(@TypeOf(modelKey), keyName)) |_| {
|
||||
// The field exists in the key structure, create its condition.
|
||||
condition.* = try modelQuery.newCondition().value(
|
||||
std.meta.fields(TableShape)[std.meta.fieldIndex(TableShape, keyName).?].type,
|
||||
keyName, "=",
|
||||
@field(modelKey, keyName),
|
||||
);
|
||||
} else {
|
||||
// The field doesn't exist, compilation error.
|
||||
@compileError("The key structure must include a field for " ++ keyName);
|
||||
}
|
||||
}
|
||||
|
||||
// Set WHERE conditions in the query.
|
||||
modelQuery.where(try modelQuery.newCondition().@"and"(&conditions));
|
||||
}
|
||||
|
||||
// Execute query and return its result.
|
||||
return try modelQuery.get(allocator);
|
||||
}
|
||||
|
||||
/// Perform creation of the given new model in the repository.
|
||||
/// The model will be altered with the inserted values.
|
||||
pub fn create(allocator: std.mem.Allocator, database: *pg.Pool, newModel: *Model) !RepositoryResult(Model) {
|
||||
// Initialize a new insert query for the given model.
|
||||
var insertQuery = Self.Insert.init(allocator, database);
|
||||
defer insertQuery.deinit();
|
||||
try insertQuery.values(newModel);
|
||||
insertQuery.returningAll();
|
||||
|
||||
// Execute insert query and get its result.
|
||||
const inserted = try insertQuery.insert(allocator);
|
||||
|
||||
if (inserted.models.len > 0) {
|
||||
// Update model with its inserted values.
|
||||
newModel.* = inserted.models[0].*;
|
||||
}
|
||||
|
||||
// Return inserted result.
|
||||
return inserted;
|
||||
}
|
||||
|
||||
/// Perform save of the given existing model in the repository.
|
||||
pub fn save(allocator: std.mem.Allocator, database: *pg.Pool, existingModel: *Model) !RepositoryResult(Model) {
|
||||
// Convert the model to its SQL form.
|
||||
const modelSql = try config.toSql(existingModel.*);
|
||||
|
||||
// Initialize a new update query for the given model.
|
||||
var updateQuery = Self.Update(TableShape).init(allocator, database);
|
||||
defer updateQuery.deinit();
|
||||
try updateQuery.set(modelSql);
|
||||
updateQuery.returningAll();
|
||||
|
||||
// Initialize conditions array.
|
||||
var conditions: [config.key.len]_sql.SqlParams = undefined;
|
||||
inline for (config.key, &conditions) |keyName, *condition| {
|
||||
// Add a where condition for each key.
|
||||
condition.* = try updateQuery.newCondition().value(@TypeOf(@field(modelSql, keyName)), keyName, "=", @field(modelSql, keyName));
|
||||
}
|
||||
// Add WHERE to the update query with built conditions.
|
||||
updateQuery.where(try updateQuery.newCondition().@"and"(&conditions));
|
||||
|
||||
// Execute update query and get its result.
|
||||
const updated = try updateQuery.update(allocator);
|
||||
|
||||
if (updated.models.len > 0) {
|
||||
// Update model with its updated values.
|
||||
existingModel.* = updated.models[0].*;
|
||||
}
|
||||
|
||||
// Return updated result.
|
||||
return updated;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// A repository query result.
|
||||
pub fn RepositoryResult(comptime Model: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
mapperArena: std.heap.ArenaAllocator,
|
||||
|
||||
/// The retrieved models.
|
||||
models: []*Model,
|
||||
/// The retrieved models collection (memory owner).
|
||||
collection: zollections.Collection(Model),
|
||||
|
||||
/// Get the first model in the list, if there is one.
|
||||
pub fn first(self: Self) ?*Model {
|
||||
if (self.models.len > 0) {
|
||||
return self.models[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize a new repository query result.
|
||||
pub fn init(allocator: std.mem.Allocator, models: zollections.Collection(Model), mapperArena: std.heap.ArenaAllocator) Self {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.mapperArena = mapperArena,
|
||||
.models = models.items,
|
||||
.collection = models,
|
||||
};
|
||||
}
|
||||
|
||||
/// Deinitialize the repository query result.
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.collection.deinit();
|
||||
self.mapperArena.deinit();
|
||||
}
|
||||
};
|
||||
}
|
19
src/root.zig
Normal file
19
src/root.zig
Normal file
|
@ -0,0 +1,19 @@
|
|||
const global = @import("global.zig");
|
||||
const repository = @import("repository.zig");
|
||||
const insert = @import("insert.zig");
|
||||
const _sql = @import("sql.zig");
|
||||
|
||||
pub const setDebug = global.setDebug;
|
||||
|
||||
pub const Repository = repository.Repository;
|
||||
pub const RepositoryConfiguration = repository.RepositoryConfiguration;
|
||||
pub const RepositoryResult = repository.RepositoryResult;
|
||||
|
||||
pub const Insertable = insert.Insertable;
|
||||
|
||||
pub const QueryParameter = _sql.QueryParameter;
|
||||
pub const SqlParams = _sql.SqlParams;
|
||||
|
||||
pub const conditions = @import("conditions.zig");
|
||||
|
||||
pub const errors = @import("errors.zig");
|
203
src/sql.zig
Normal file
203
src/sql.zig
Normal file
|
@ -0,0 +1,203 @@
|
|||
const std = @import("std");
|
||||
const errors = @import("errors.zig");
|
||||
|
||||
/// A structure with SQL and its parameters.
|
||||
pub const SqlParams = struct {
|
||||
sql: []const u8,
|
||||
params: []const QueryParameter,
|
||||
};
|
||||
|
||||
/// 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, 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;
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
pub fn copyAndReplaceSqlParameters(currentParameter: *usize, parametersCount: usize, dest: []u8, source: []const u8) !void {
|
||||
// If there are no parameters, just copy source SQL.
|
||||
if (parametersCount <= 0) {
|
||||
std.mem.copyForwards(u8, dest, source);
|
||||
}
|
||||
|
||||
// Current dest cursor.
|
||||
var destCursor: usize = 0;
|
||||
|
||||
for (source) |char| {
|
||||
// Copy each character but '?', replaced by the current parameter string.
|
||||
|
||||
if (char == '?') {
|
||||
// Create the parameter string.
|
||||
const paramSize = 1 + try computeRequiredSpaceForParameter(currentParameter.*);
|
||||
// Copy the parameter string in place of '?'.
|
||||
_ = try std.fmt.bufPrint(dest[destCursor..destCursor+paramSize], "${d}", .{currentParameter.*});
|
||||
// Add parameter string length to the current query cursor.
|
||||
destCursor += paramSize;
|
||||
// Increment parameter count.
|
||||
currentParameter.* += 1;
|
||||
} else {
|
||||
// Simply pass the current character.
|
||||
dest[destCursor] = 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
|
||||
};
|
||||
}
|
||||
};
|
350
src/update.zig
Normal file
350
src/update.zig
Normal file
|
@ -0,0 +1,350 @@
|
|||
const std = @import("std");
|
||||
const pg = @import("pg");
|
||||
const zollections = @import("zollections");
|
||||
const errors = @import("errors.zig");
|
||||
const postgresql = @import("postgresql.zig");
|
||||
const _sql = @import("sql.zig");
|
||||
const conditions = @import("conditions.zig");
|
||||
const repository = @import("repository.zig");
|
||||
|
||||
/// Repository update query configuration structure.
|
||||
pub fn RepositoryUpdateConfiguration(comptime UpdateShape: type) type {
|
||||
return struct {
|
||||
value: ?UpdateShape = null,
|
||||
where: ?_sql.SqlParams = null,
|
||||
returning: ?_sql.SqlParams = null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Repository models update manager.
|
||||
/// Manage update query string build and execution.
|
||||
pub fn RepositoryUpdate(comptime Model: type, comptime TableShape: type, comptime repositoryConfig: repository.RepositoryConfiguration(Model, TableShape), comptime UpdateShape: type) type {
|
||||
// Create columns list.
|
||||
const columns = comptime columnsFinder: {
|
||||
// Get update shape type data.
|
||||
const updateType = @typeInfo(UpdateShape);
|
||||
// Initialize a columns slice of "fields len" size.
|
||||
var columnsList: [updateType.Struct.fields.len][]const u8 = undefined;
|
||||
|
||||
// Add structure fields to the columns slice.
|
||||
var i: usize = 0;
|
||||
for (updateType.Struct.fields) |field| {
|
||||
// Check that the table type defines the same fields.
|
||||
if (!@hasField(TableShape, field.name))
|
||||
//TODO check its type?
|
||||
@compileError("The table doesn't contain the indicated updated columns.");
|
||||
|
||||
// Add each structure field to columns list.
|
||||
columnsList[i] = field.name;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// Assign built columns list.
|
||||
break :columnsFinder columnsList;
|
||||
};
|
||||
|
||||
// Pre-compute SQL buffer size.
|
||||
const sqlBase = "UPDATE " ++ repositoryConfig.table ++ " SET ";
|
||||
const whereClause = "WHERE";
|
||||
const returningClause = "RETURNING";
|
||||
|
||||
// UPDATE {repositoryConfig.table} SET ?;
|
||||
const fixedSqlSize = sqlBase.len + 0 + 1;
|
||||
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
const Configuration = RepositoryUpdateConfiguration(UpdateShape);
|
||||
|
||||
arena: std.heap.ArenaAllocator,
|
||||
database: *pg.Pool,
|
||||
updateConfig: Configuration,
|
||||
|
||||
sql: ?[]const u8 = null,
|
||||
|
||||
/// Parse given model or shape and put the result in newUpdate.
|
||||
fn parseData(newUpdate: *UpdateShape, _value: anytype) !void {
|
||||
// If the given value is a model, first convert it to its SQL equivalent.
|
||||
if (@TypeOf(_value) == Model) {
|
||||
return parseData(newUpdate, try repositoryConfig.toSql(_value));
|
||||
}
|
||||
|
||||
inline for (columns) |column| {
|
||||
// Assign every given value to the update shape.
|
||||
@field(newUpdate.*, column) = @field(_value, column);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse one "updates value".
|
||||
fn parseOne(self: *Self, _value: anytype) !void {
|
||||
const newUpdate = try self.arena.allocator().create(UpdateShape);
|
||||
try parseData(newUpdate, _value);
|
||||
self.updateConfig.value = newUpdate.*;
|
||||
}
|
||||
|
||||
/// Set updated values.
|
||||
/// Values can be Model, TableShape or UpdateShape.
|
||||
pub fn set(self: *Self, _value: anytype) !void {
|
||||
// Get value type.
|
||||
const valueType = @TypeOf(_value);
|
||||
|
||||
switch (@typeInfo(valueType)) {
|
||||
.Pointer => |ptr| {
|
||||
switch (ptr.size) {
|
||||
// It's a single object.
|
||||
.One => switch (@typeInfo(ptr.child)) {
|
||||
// It's a structure, parse it.
|
||||
.Struct => try self.parseOne(_value.*),
|
||||
// It's not a structure: cannot parse it.
|
||||
else => @compileError("Cannot set update value of type " ++ @typeName(ptr.child)),
|
||||
},
|
||||
// It's not a single object: cannot parse it.
|
||||
else => @compileError("Cannot set update value of type " ++ @typeName(ptr.child)),
|
||||
}
|
||||
},
|
||||
// It's a structure, just parse it.
|
||||
.Struct => try self.parseOne(_value),
|
||||
|
||||
// It's not a structure nor a pointer to a structure: cannot parse it.
|
||||
else => @compileError("Cannot set update value of type " ++ @typeName(valueType)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set WHERE conditions.
|
||||
pub fn where(self: *Self, _where: _sql.SqlParams) void {
|
||||
self.updateConfig.where = _where;
|
||||
}
|
||||
|
||||
/// Create a new condition builder.
|
||||
pub fn newCondition(self: *Self) conditions.Builder {
|
||||
return conditions.Builder.init(self.arena.allocator());
|
||||
}
|
||||
|
||||
/// Set a WHERE value condition.
|
||||
pub fn whereValue(self: *Self, comptime ValueType: type, comptime _column: []const u8, comptime operator: []const u8, _value: ValueType) !void {
|
||||
self.where(
|
||||
try conditions.value(ValueType, self.arena.allocator(), _column, operator, _value)
|
||||
);
|
||||
}
|
||||
|
||||
/// Set a WHERE column condition.
|
||||
pub fn whereColumn(self: *Self, comptime _column: []const u8, comptime operator: []const u8, comptime _valueColumn: []const u8) !void {
|
||||
self.where(
|
||||
try conditions.column(self.arena.allocator(), _column, operator, _valueColumn)
|
||||
);
|
||||
}
|
||||
|
||||
/// Set a WHERE IN condition.
|
||||
pub fn whereIn(self: *Self, comptime ValueType: type, comptime _column: []const u8, _value: []const ValueType) !void {
|
||||
self.where(
|
||||
try conditions.in(ValueType, self.arena.allocator(), _column, _value)
|
||||
);
|
||||
}
|
||||
|
||||
/// Set selected columns for RETURNING clause.
|
||||
pub fn returning(self: *Self, _select: _sql.SqlParams) void {
|
||||
self.updateConfig.returning = _select;
|
||||
}
|
||||
|
||||
/// Set selected columns for RETURNING clause.
|
||||
pub fn returningColumns(self: *Self, _select: []const []const u8) void {
|
||||
if (_select.len == 0) {
|
||||
return errors.AtLeastOneSelectionRequired;
|
||||
}
|
||||
|
||||
self.returning(.{
|
||||
// Join selected columns.
|
||||
.sql = std.mem.join(self.arena.allocator(), ", ", _select),
|
||||
.params = &[_]_sql.QueryParameter{}, // No parameters.
|
||||
});
|
||||
}
|
||||
|
||||
/// Set RETURNING all columns of the table after update.
|
||||
pub fn returningAll(self: *Self) void {
|
||||
self.returning(.{
|
||||
.sql = "*",
|
||||
.params = &[_]_sql.QueryParameter{}, // No parameters.
|
||||
});
|
||||
}
|
||||
|
||||
/// Build SQL query.
|
||||
pub fn buildSql(self: *Self) !void {
|
||||
if (self.updateConfig.value) |_| {} else {
|
||||
// Updated values must be set.
|
||||
return errors.ZrmError.UpdatedValuesRequired;
|
||||
}
|
||||
|
||||
// Start parameter counter at 1.
|
||||
var currentParameter: usize = 1;
|
||||
|
||||
// Compute SET values size.
|
||||
var setSize: usize = 0;
|
||||
inline for (columns) |column| {
|
||||
// Compute size of each column value assignment.
|
||||
setSize += column.len + 1 + 1 + try _sql.computeRequiredSpaceForParameter(currentParameter) + 1;
|
||||
currentParameter += 1;
|
||||
}
|
||||
setSize -= 1; // The last ',' can be overwritten.
|
||||
|
||||
// Compute WHERE size.
|
||||
var whereSize: usize = 0;
|
||||
if (self.updateConfig.where) |_where| {
|
||||
whereSize = 1 + whereClause.len + 1 + _where.sql.len + _sql.computeRequiredSpaceForParametersNumbers(_where.params.len, currentParameter - 1);
|
||||
currentParameter += _where.params.len;
|
||||
}
|
||||
|
||||
// Compute RETURNING size.
|
||||
var returningSize: usize = 0;
|
||||
if (self.updateConfig.returning) |_returning| {
|
||||
returningSize = 1 + returningClause.len + _returning.sql.len + 1 + _sql.computeRequiredSpaceForParametersNumbers(_returning.params.len, currentParameter - 1);
|
||||
currentParameter += _returning.params.len;
|
||||
}
|
||||
|
||||
// Allocate SQL buffer from computed size.
|
||||
const sqlBuf = try self.arena.allocator().alloc(u8, fixedSqlSize
|
||||
+ (setSize)
|
||||
+ (whereSize)
|
||||
+ (returningSize)
|
||||
);
|
||||
|
||||
// Fill SQL buffer.
|
||||
|
||||
// Restart parameter counter at 1.
|
||||
currentParameter = 1;
|
||||
|
||||
// SQL query initialisation.
|
||||
@memcpy(sqlBuf[0..sqlBase.len], sqlBase);
|
||||
var sqlBufCursor: usize = sqlBase.len;
|
||||
|
||||
// Add SET columns values.
|
||||
inline for (columns) |column| {
|
||||
// Create the SET string and append it to the SQL buffer.
|
||||
const setColumnSize = column.len + 1 + 1 + try _sql.computeRequiredSpaceForParameter(currentParameter) + 1;
|
||||
_ = try std.fmt.bufPrint(sqlBuf[sqlBufCursor..sqlBufCursor+setColumnSize], "{s}=${d},", .{column, currentParameter});
|
||||
sqlBufCursor += setColumnSize;
|
||||
// Increment parameter count.
|
||||
currentParameter += 1;
|
||||
}
|
||||
|
||||
// Overwrite the last ','.
|
||||
sqlBufCursor -= 1;
|
||||
|
||||
// WHERE clause.
|
||||
if (self.updateConfig.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;
|
||||
}
|
||||
|
||||
// Append RETURNING clause, if there is one defined.
|
||||
if (self.updateConfig.returning) |_returning| {
|
||||
@memcpy(sqlBuf[sqlBufCursor..sqlBufCursor+(1 + returningClause.len + 1)], " " ++ returningClause ++ " ");
|
||||
// Copy RETURNING clause content and replace parameters, if there are some.
|
||||
try _sql.copyAndReplaceSqlParameters(¤tParameter,
|
||||
_returning.params.len,
|
||||
sqlBuf[sqlBufCursor+(1+returningClause.len+1)..sqlBufCursor+returningSize], _returning.sql
|
||||
);
|
||||
sqlBufCursor += returningSize;
|
||||
}
|
||||
|
||||
// ";" to end the query.
|
||||
sqlBuf[sqlBufCursor] = ';'; sqlBufCursor += 1;
|
||||
|
||||
// Save built SQL query.
|
||||
self.sql = sqlBuf;
|
||||
}
|
||||
|
||||
/// Execute the update query.
|
||||
fn execQuery(self: *Self) !*pg.Result {
|
||||
// Get a connection to the database.
|
||||
const connection = try self.database.acquire();
|
||||
errdefer connection.release();
|
||||
|
||||
// Initialize a new PostgreSQL statement.
|
||||
var statement = try pg.Stmt.init(connection, .{
|
||||
.column_names = true,
|
||||
.release_conn = true,
|
||||
.allocator = self.arena.allocator(),
|
||||
});
|
||||
errdefer statement.deinit();
|
||||
|
||||
// Prepare SQL update query.
|
||||
statement.prepare(self.sql.?)
|
||||
catch |err| return postgresql.handlePostgresqlError(err, connection, &statement);
|
||||
|
||||
// Bind UPDATE query parameters.
|
||||
inline for (columns) |column| {
|
||||
try statement.bind(@field(self.updateConfig.value.?, column));
|
||||
}
|
||||
// Bind WHERE query parameters.
|
||||
if (self.updateConfig.where) |_where| {
|
||||
try postgresql.bindQueryParameters(&statement, _where.params);
|
||||
}
|
||||
// Bind RETURNING query parameters.
|
||||
if (self.updateConfig.returning) |_returning| {
|
||||
try postgresql.bindQueryParameters(&statement, _returning.params);
|
||||
}
|
||||
|
||||
// Execute the query and get its result.
|
||||
const result = statement.execute()
|
||||
catch |err| return postgresql.handlePostgresqlError(err, connection, &statement);
|
||||
|
||||
// Query executed successfully, return the result.
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Update given models.
|
||||
pub fn update(self: *Self, allocator: std.mem.Allocator) !repository.RepositoryResult(Model) {
|
||||
// Build SQL query if it wasn't built.
|
||||
if (self.sql) |_| {} else { try self.buildSql(); }
|
||||
|
||||
// Execute query and get its result.
|
||||
const queryResult = try self.execQuery();
|
||||
|
||||
//TODO deduplicate this in postgresql.zig, we could do it if Mapper type was exposed.
|
||||
//TODO make a generic mapper and do it in repository.zig?
|
||||
// Create an arena for mapper data.
|
||||
var mapperArena = std.heap.ArenaAllocator.init(allocator);
|
||||
// Get result mapper.
|
||||
const mapper = queryResult.mapper(TableShape, .{ .allocator = mapperArena.allocator() });
|
||||
|
||||
// Initialize models list.
|
||||
var models = std.ArrayList(*Model).init(allocator);
|
||||
defer models.deinit();
|
||||
|
||||
// Get all raw models from the result mapper.
|
||||
while (try mapper.next()) |rawModel| {
|
||||
// Parse each raw model from the mapper.
|
||||
const model = try allocator.create(Model);
|
||||
model.* = try repositoryConfig.fromSql(rawModel);
|
||||
try models.append(model);
|
||||
}
|
||||
|
||||
// Return a result with the models.
|
||||
return repository.RepositoryResult(Model).init(allocator,
|
||||
zollections.Collection(Model).init(allocator, try models.toOwnedSlice()),
|
||||
mapperArena,
|
||||
);
|
||||
}
|
||||
|
||||
/// Initialize a new repository update query.
|
||||
pub fn init(allocator: std.mem.Allocator, database: *pg.Pool) Self {
|
||||
return .{
|
||||
// Initialize an arena allocator for the update query.
|
||||
.arena = std.heap.ArenaAllocator.init(allocator),
|
||||
.database = database,
|
||||
.updateConfig = .{},
|
||||
};
|
||||
}
|
||||
|
||||
/// Deinitialize the repository update query.
|
||||
pub fn deinit(self: *Self) void {
|
||||
// Free everything allocated for this update query.
|
||||
self.arena.deinit();
|
||||
}
|
||||
};
|
||||
}
|
139
tests/composite.zig
Normal file
139
tests/composite.zig
Normal file
|
@ -0,0 +1,139 @@
|
|||
const std = @import("std");
|
||||
const pg = @import("pg");
|
||||
const zrm = @import("zrm");
|
||||
|
||||
/// PostgreSQL database connection.
|
||||
var database: *pg.Pool = undefined;
|
||||
|
||||
/// Initialize database connection.
|
||||
fn initDatabase() !void {
|
||||
database = try pg.Pool.init(std.testing.allocator, .{
|
||||
.connect = .{
|
||||
.host = "localhost",
|
||||
.port = 5432,
|
||||
},
|
||||
.auth = .{
|
||||
.username = "zrm",
|
||||
.password = "zrm",
|
||||
.database = "zrm",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/// An example model with composite key.
|
||||
const CompositeModel = struct {
|
||||
firstcol: i32,
|
||||
secondcol: []const u8,
|
||||
|
||||
label: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
/// SQL table shape of the example model with composite key.
|
||||
const CompositeModelTable = struct {
|
||||
firstcol: i32,
|
||||
secondcol: []const u8,
|
||||
|
||||
label: ?[]const u8,
|
||||
};
|
||||
|
||||
// Convert an SQL row to a model.
|
||||
fn modelFromSql(raw: CompositeModelTable) !CompositeModel {
|
||||
return .{
|
||||
.firstcol = raw.firstcol,
|
||||
.secondcol = raw.secondcol,
|
||||
.label = raw.label,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert a model to an SQL row.
|
||||
fn modelToSql(model: CompositeModel) !CompositeModelTable {
|
||||
return .{
|
||||
.firstcol = model.firstcol,
|
||||
.secondcol = model.secondcol,
|
||||
.label = model.label,
|
||||
};
|
||||
}
|
||||
|
||||
/// Declare the composite model repository.
|
||||
const CompositeModelRepository = zrm.Repository(CompositeModel, CompositeModelTable, .{
|
||||
.table = "composite_models",
|
||||
|
||||
// Insert shape used by default for inserts in the repository.
|
||||
.insertShape = struct {
|
||||
secondcol: zrm.Insertable([]const u8),
|
||||
label: zrm.Insertable([]const u8),
|
||||
},
|
||||
|
||||
.key = &[_][]const u8{"firstcol", "secondcol"},
|
||||
|
||||
.fromSql = &modelFromSql,
|
||||
.toSql = &modelToSql,
|
||||
});
|
||||
|
||||
|
||||
test "composite model create, save and find" {
|
||||
zrm.setDebug(true);
|
||||
|
||||
try initDatabase();
|
||||
defer database.deinit();
|
||||
|
||||
// Initialize a test model.
|
||||
var newModel = CompositeModel{
|
||||
.firstcol = 0,
|
||||
.secondcol = "identifier",
|
||||
.label = "test label",
|
||||
};
|
||||
|
||||
|
||||
// Create the new model.
|
||||
var result = try CompositeModelRepository.create(std.testing.allocator, database, &newModel);
|
||||
defer result.deinit(); // Will clear some values in newModel.
|
||||
|
||||
// Check that the model is correctly defined.
|
||||
try std.testing.expect(newModel.firstcol > 0);
|
||||
try std.testing.expectEqualStrings("identifier", newModel.secondcol);
|
||||
try std.testing.expectEqualStrings("test label", newModel.label.?);
|
||||
|
||||
|
||||
const postInsertFirstcol = newModel.firstcol;
|
||||
const postInsertSecondcol = newModel.secondcol;
|
||||
|
||||
// Update the model.
|
||||
newModel.label = null;
|
||||
|
||||
var result2 = try CompositeModelRepository.save(std.testing.allocator, database, &newModel);
|
||||
defer result2.deinit(); // Will clear some values in newModel.
|
||||
|
||||
// Checking that the model has been updated (but only the right field).
|
||||
try std.testing.expectEqual(postInsertFirstcol, newModel.firstcol);
|
||||
try std.testing.expectEqualStrings(postInsertSecondcol, newModel.secondcol);
|
||||
try std.testing.expectEqual(null, newModel.label);
|
||||
|
||||
|
||||
// Do another insert with the same secondcol.
|
||||
var insertQuery = CompositeModelRepository.Insert.init(std.testing.allocator, database);
|
||||
defer insertQuery.deinit();
|
||||
try insertQuery.values(.{
|
||||
.secondcol = "identifier",
|
||||
.label = "test",
|
||||
});
|
||||
insertQuery.returningAll();
|
||||
var result3 = try insertQuery.insert(std.testing.allocator);
|
||||
defer result3.deinit();
|
||||
|
||||
// Checking that the other model has been inserted correctly.
|
||||
try std.testing.expect(result3.first().?.firstcol > newModel.firstcol);
|
||||
try std.testing.expectEqualStrings("identifier", result3.first().?.secondcol);
|
||||
try std.testing.expectEqualStrings("test", result3.first().?.label.?);
|
||||
|
||||
|
||||
// Try to find the created then saved model, to check that everything has been saved correctly.
|
||||
var result4 = try CompositeModelRepository.find(std.testing.allocator, database, .{
|
||||
.firstcol = newModel.firstcol,
|
||||
.secondcol = newModel.secondcol,
|
||||
});
|
||||
defer result4.deinit(); // Will clear some values in newModel.
|
||||
|
||||
try std.testing.expectEqual(1, result4.models.len);
|
||||
try std.testing.expectEqualDeep(newModel, result4.first().?.*);
|
||||
}
|
21
tests/initdb.sql
Normal file
21
tests/initdb.sql
Normal file
|
@ -0,0 +1,21 @@
|
|||
-- Cleanup existing database content.
|
||||
DROP SCHEMA public CASCADE;
|
||||
CREATE SCHEMA public;
|
||||
|
||||
-- Create default models table.
|
||||
CREATE TABLE models (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
amount NUMERIC(12, 2) NOT NULL
|
||||
);
|
||||
|
||||
-- Insert default data.
|
||||
INSERT INTO models(name, amount) VALUES ('test', 50);
|
||||
|
||||
-- Create default composite models table.
|
||||
CREATE TABLE composite_models (
|
||||
firstcol SERIAL NOT NULL,
|
||||
secondcol VARCHAR NOT NULL,
|
||||
label VARCHAR NULL,
|
||||
PRIMARY KEY (firstcol, secondcol)
|
||||
);
|
56
tests/query.zig
Normal file
56
tests/query.zig
Normal file
|
@ -0,0 +1,56 @@
|
|||
const std = @import("std");
|
||||
const zrm = @import("zrm");
|
||||
|
||||
test "zrm.conditions.value" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const condition = try zrm.conditions.value(usize, arena.allocator(), "test", "=", 5);
|
||||
|
||||
try std.testing.expectEqualStrings("test = ?", condition.sql);
|
||||
try std.testing.expectEqual(1, condition.params.len);
|
||||
try std.testing.expectEqual(5, condition.params[0].integer);
|
||||
}
|
||||
|
||||
test "zrm.conditions.in" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const condition = try zrm.conditions.in(usize, arena.allocator(), "intest", &[_]usize{2, 3, 8});
|
||||
|
||||
try std.testing.expectEqualStrings("intest IN (?,?,?)", condition.sql);
|
||||
try std.testing.expectEqual(3, condition.params.len);
|
||||
try std.testing.expectEqual(2, condition.params[0].integer);
|
||||
try std.testing.expectEqual(3, condition.params[1].integer);
|
||||
try std.testing.expectEqual(8, condition.params[2].integer);
|
||||
}
|
||||
|
||||
test "zrm.conditions.column" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const condition = try zrm.conditions.column(arena.allocator(), "firstcol", "<>", "secondcol");
|
||||
|
||||
try std.testing.expectEqualStrings("firstcol <> secondcol", condition.sql);
|
||||
try std.testing.expectEqual(0, condition.params.len);
|
||||
}
|
||||
|
||||
test "zrm.conditions combined" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const condition = try zrm.conditions.@"and"(arena.allocator(), &[_]zrm.SqlParams{
|
||||
try zrm.conditions.value(usize, arena.allocator(), "test", "=", 5),
|
||||
try zrm.conditions.@"or"(arena.allocator(), &[_]zrm.SqlParams{
|
||||
try zrm.conditions.in(usize, arena.allocator(), "intest", &[_]usize{2, 3, 8}),
|
||||
try zrm.conditions.column(arena.allocator(), "firstcol", "<>", "secondcol"),
|
||||
}),
|
||||
});
|
||||
|
||||
try std.testing.expectEqualStrings("(test = ? AND (intest IN (?,?,?) OR firstcol <> secondcol))", condition.sql);
|
||||
try std.testing.expectEqual(4, condition.params.len);
|
||||
try std.testing.expectEqual(5, condition.params[0].integer);
|
||||
try std.testing.expectEqual(2, condition.params[1].integer);
|
||||
try std.testing.expectEqual(3, condition.params[2].integer);
|
||||
try std.testing.expectEqual(8, condition.params[3].integer);
|
||||
}
|
313
tests/repository.zig
Normal file
313
tests/repository.zig
Normal file
|
@ -0,0 +1,313 @@
|
|||
const std = @import("std");
|
||||
const pg = @import("pg");
|
||||
const zrm = @import("zrm");
|
||||
|
||||
/// PostgreSQL database connection.
|
||||
var database: *pg.Pool = undefined;
|
||||
|
||||
/// Initialize database connection.
|
||||
fn initDatabase() !void {
|
||||
database = try pg.Pool.init(std.testing.allocator, .{
|
||||
.connect = .{
|
||||
.host = "localhost",
|
||||
.port = 5432,
|
||||
},
|
||||
.auth = .{
|
||||
.username = "zrm",
|
||||
.password = "zrm",
|
||||
.database = "zrm",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/// An example submodel, child of the example model.
|
||||
const MySubmodel = struct {
|
||||
uuid: []const u8,
|
||||
label: []const u8,
|
||||
|
||||
parent: ?MyModel = null,
|
||||
};
|
||||
|
||||
/// An example model.
|
||||
const MyModel = struct {
|
||||
id: i32,
|
||||
name: []const u8,
|
||||
amount: f64,
|
||||
|
||||
submodels: ?[]const MySubmodel = null,
|
||||
};
|
||||
|
||||
/// SQL table shape of the example model.
|
||||
const MyModelTable = struct {
|
||||
id: i32,
|
||||
name: []const u8,
|
||||
amount: f64,
|
||||
};
|
||||
|
||||
// Convert an SQL row to a model.
|
||||
fn modelFromSql(raw: MyModelTable) !MyModel {
|
||||
return .{
|
||||
.id = raw.id,
|
||||
.name = raw.name,
|
||||
.amount = raw.amount,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert a model to an SQL row.
|
||||
fn modelToSql(model: MyModel) !MyModelTable {
|
||||
return .{
|
||||
.id = model.id,
|
||||
.name = model.name,
|
||||
.amount = model.amount,
|
||||
};
|
||||
}
|
||||
|
||||
/// Declare a model repository.
|
||||
const MyModelRepository = zrm.Repository(MyModel, MyModelTable, .{
|
||||
.table = "models",
|
||||
|
||||
// Insert shape used by default for inserts in the repository.
|
||||
.insertShape = struct {
|
||||
name: zrm.Insertable([]const u8),
|
||||
amount: zrm.Insertable(f64),
|
||||
},
|
||||
|
||||
.key = &[_][]const u8{"id"},
|
||||
|
||||
.fromSql = &modelFromSql,
|
||||
.toSql = &modelToSql,
|
||||
});
|
||||
|
||||
|
||||
test "model structures" {
|
||||
// Initialize a test model.
|
||||
const testModel = MyModel{
|
||||
.id = 10,
|
||||
.name = "test",
|
||||
.amount = 15.5,
|
||||
|
||||
.submodels = &[_]MySubmodel{
|
||||
MySubmodel{
|
||||
.uuid = "56c378bf-cfda-4438-9b33-b4c63f190907",
|
||||
.label = "test",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Test that the model is correctly initialized.
|
||||
try std.testing.expectEqual(10, testModel.id);
|
||||
try std.testing.expectEqualStrings("56c378bf-cfda-4438-9b33-b4c63f190907", testModel.submodels.?[0].uuid);
|
||||
}
|
||||
|
||||
|
||||
test "repository query SQL builder" {
|
||||
zrm.setDebug(true);
|
||||
|
||||
try initDatabase();
|
||||
defer database.deinit();
|
||||
|
||||
var query = MyModelRepository.Query.init(std.testing.allocator, database, .{});
|
||||
defer query.deinit();
|
||||
try query.whereIn(usize, "id", &[_]usize{1, 2});
|
||||
try query.buildSql();
|
||||
|
||||
const expectedSql = "SELECT * FROM models WHERE id IN ($1,$2);";
|
||||
try std.testing.expectEqual(expectedSql.len, query.sql.?.len);
|
||||
try std.testing.expectEqualStrings(expectedSql, query.sql.?);
|
||||
}
|
||||
|
||||
test "repository element retrieval" {
|
||||
zrm.setDebug(true);
|
||||
|
||||
try initDatabase();
|
||||
defer database.deinit();
|
||||
|
||||
// Prepare a query for models.
|
||||
var query = MyModelRepository.Query.init(std.testing.allocator, database, .{});
|
||||
try query.whereValue(usize, "id", "=", 1);
|
||||
defer query.deinit();
|
||||
|
||||
// Build SQL.
|
||||
try query.buildSql();
|
||||
|
||||
// Check built SQL.
|
||||
const expectedSql = "SELECT * FROM models WHERE id = $1;";
|
||||
try std.testing.expectEqual(expectedSql.len, query.sql.?.len);
|
||||
try std.testing.expectEqualStrings(expectedSql, query.sql.?);
|
||||
|
||||
// Get models.
|
||||
var result = try query.get(std.testing.allocator);
|
||||
defer result.deinit();
|
||||
|
||||
// Check that one model has been retrieved, then check its type and values.
|
||||
try std.testing.expectEqual(1, result.models.len);
|
||||
try std.testing.expectEqual(MyModel, @TypeOf(result.models[0].*));
|
||||
try std.testing.expectEqual(1, result.models[0].id);
|
||||
try std.testing.expectEqualStrings("test", result.models[0].name);
|
||||
try std.testing.expectEqual(50.00, result.models[0].amount);
|
||||
}
|
||||
|
||||
test "repository complex SQL query" {
|
||||
zrm.setDebug(true);
|
||||
|
||||
try initDatabase();
|
||||
defer database.deinit();
|
||||
|
||||
var query = MyModelRepository.Query.init(std.testing.allocator, database, .{});
|
||||
defer query.deinit();
|
||||
query.where(
|
||||
try query.newCondition().@"or"(&[_]zrm.SqlParams{
|
||||
try query.newCondition().value(usize, "id", "=", 1),
|
||||
try query.newCondition().@"and"(&[_]zrm.SqlParams{
|
||||
try query.newCondition().in(usize, "id", &[_]usize{100000, 200000, 300000}),
|
||||
try query.newCondition().@"or"(&[_]zrm.SqlParams{
|
||||
try query.newCondition().value(f64, "amount", ">", 12.13),
|
||||
try query.newCondition().value([]const u8, "name", "=", "test"),
|
||||
})
|
||||
}),
|
||||
})
|
||||
);
|
||||
try query.buildSql();
|
||||
|
||||
const expectedSql = "SELECT * 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.expectEqualStrings(expectedSql, query.sql.?);
|
||||
|
||||
// Get models.
|
||||
var result = try query.get(std.testing.allocator);
|
||||
defer result.deinit();
|
||||
|
||||
// Check that one model has been retrieved.
|
||||
try std.testing.expectEqual(1, result.models.len);
|
||||
try std.testing.expectEqual(1, result.models[0].id);
|
||||
}
|
||||
|
||||
test "repository element creation" {
|
||||
zrm.setDebug(true);
|
||||
|
||||
try initDatabase();
|
||||
defer database.deinit();
|
||||
|
||||
// Create a model to insert.
|
||||
const newModel = MyModel{
|
||||
.id = undefined,
|
||||
.amount = 75,
|
||||
.name = "inserted model",
|
||||
};
|
||||
|
||||
// Initialize an insert query.
|
||||
var insertQuery = MyModelRepository.Insert.init(std.testing.allocator, database);
|
||||
defer insertQuery.deinit();
|
||||
// Insert the new model.
|
||||
try insertQuery.values(newModel);
|
||||
insertQuery.returningAll();
|
||||
|
||||
// Build SQL.
|
||||
try insertQuery.buildSql();
|
||||
|
||||
// Check built SQL.
|
||||
const expectedSql = "INSERT INTO models(name,amount) VALUES ($1,$2) RETURNING *;";
|
||||
try std.testing.expectEqual(expectedSql.len, insertQuery.sql.?.len);
|
||||
try std.testing.expectEqualStrings(expectedSql, insertQuery.sql.?);
|
||||
|
||||
// Insert models.
|
||||
var result = try insertQuery.insert(std.testing.allocator);
|
||||
defer result.deinit();
|
||||
|
||||
// Check the inserted model.
|
||||
try std.testing.expectEqual(1, result.models.len);
|
||||
try std.testing.expectEqual(75, result.models[0].amount);
|
||||
try std.testing.expectEqualStrings("inserted model", result.models[0].name);
|
||||
}
|
||||
|
||||
test "repository element update" {
|
||||
zrm.setDebug(true);
|
||||
|
||||
try initDatabase();
|
||||
defer database.deinit();
|
||||
|
||||
// Initialize an update query.
|
||||
var updateQuery = MyModelRepository.Update(struct {
|
||||
name: []const u8,
|
||||
}).init(std.testing.allocator, database);
|
||||
defer updateQuery.deinit();
|
||||
|
||||
// Update a model's name.
|
||||
try updateQuery.set(.{ .name = "newname" });
|
||||
try updateQuery.whereValue(usize, "id", "=", 1);
|
||||
updateQuery.returningAll();
|
||||
|
||||
// Build SQL.
|
||||
try updateQuery.buildSql();
|
||||
|
||||
// Check built SQL.
|
||||
const expectedSql = "UPDATE models SET name=$1 WHERE id = $2 RETURNING *;";
|
||||
try std.testing.expectEqual(expectedSql.len, updateQuery.sql.?.len);
|
||||
try std.testing.expectEqualStrings(expectedSql, updateQuery.sql.?);
|
||||
|
||||
// Update models.
|
||||
var result = try updateQuery.update(std.testing.allocator);
|
||||
defer result.deinit();
|
||||
|
||||
// Check the updated model.
|
||||
try std.testing.expectEqual(1, result.models.len);
|
||||
try std.testing.expectEqual(1, result.models[0].id);
|
||||
try std.testing.expectEqualStrings("newname", result.models[0].name);
|
||||
}
|
||||
|
||||
test "model create, save and find" {
|
||||
zrm.setDebug(true);
|
||||
|
||||
try initDatabase();
|
||||
defer database.deinit();
|
||||
|
||||
// Initialize a test model.
|
||||
var newModel = MyModel{
|
||||
.id = 0,
|
||||
.amount = 555,
|
||||
.name = "newly created model",
|
||||
};
|
||||
|
||||
|
||||
// Create the new model.
|
||||
var result = try MyModelRepository.create(std.testing.allocator, database, &newModel);
|
||||
defer result.deinit(); // Will clear some values in newModel.
|
||||
|
||||
// Check that the model is correctly defined.
|
||||
try std.testing.expect(newModel.id > 1);
|
||||
try std.testing.expectEqualStrings("newly created model", newModel.name);
|
||||
|
||||
|
||||
const postInsertId = newModel.id;
|
||||
const postInsertAmount = newModel.amount;
|
||||
|
||||
// Update the model.
|
||||
newModel.name = "recently updated name";
|
||||
|
||||
var result2 = try MyModelRepository.save(std.testing.allocator, database, &newModel);
|
||||
defer result2.deinit(); // Will clear some values in newModel.
|
||||
|
||||
// Checking that the model has been updated (but only the right field).
|
||||
try std.testing.expectEqual(postInsertId, newModel.id);
|
||||
try std.testing.expectEqualStrings("recently updated name", newModel.name);
|
||||
try std.testing.expectEqual(postInsertAmount, newModel.amount);
|
||||
|
||||
|
||||
// Do another update.
|
||||
newModel.amount = 12.226;
|
||||
|
||||
var result3 = try MyModelRepository.save(std.testing.allocator, database, &newModel);
|
||||
defer result3.deinit(); // Will clear some values in newModel.
|
||||
|
||||
// Checking that the model has been updated (but only the right field).
|
||||
try std.testing.expectEqual(postInsertId, newModel.id);
|
||||
try std.testing.expectEqualStrings("recently updated name", newModel.name);
|
||||
try std.testing.expectEqual(12.23, newModel.amount);
|
||||
|
||||
|
||||
// Try to find the created then saved model, to check that everything has been saved correctly.
|
||||
var result4 = try MyModelRepository.find(std.testing.allocator, database, newModel.id);
|
||||
defer result4.deinit(); // Will clear some values in newModel.
|
||||
|
||||
try std.testing.expectEqualDeep(newModel, result4.first().?.*);
|
||||
}
|
7
tests/root.zig
Normal file
7
tests/root.zig
Normal file
|
@ -0,0 +1,7 @@
|
|||
const std = @import("std");
|
||||
|
||||
comptime {
|
||||
_ = @import("query.zig");
|
||||
_ = @import("repository.zig");
|
||||
_ = @import("composite.zig");
|
||||
}
|
25
tests/setup.zig
Normal file
25
tests/setup.zig
Normal file
|
@ -0,0 +1,25 @@
|
|||
const std = @import("std");
|
||||
const pg = @import("pg");
|
||||
|
||||
/// PostgreSQL database connection.
|
||||
var database: *pg.Pool = undefined;
|
||||
|
||||
/// Initialize database connection.
|
||||
fn initDatabase() !void {
|
||||
database = try pg.Pool.init(std.heap.page_allocator, .{
|
||||
.connect = .{
|
||||
.host = "localhost",
|
||||
.port = 5432,
|
||||
},
|
||||
.auth = .{
|
||||
.username = "zrm",
|
||||
.password = "zrm",
|
||||
.database = "zrm",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
pub fn main() !void {
|
||||
try initDatabase();
|
||||
_ = try database.exec(@embedFile("initdb.sql"), .{});
|
||||
}
|
Loading…
Reference in a new issue