Compare commits
10 commits
Author | SHA1 | Date | |
---|---|---|---|
f186b99fde | |||
cc4c808937 | |||
fee00d5bc9 | |||
7e31368fa3 | |||
0132ec282b | |||
2f93383aa5 | |||
650d2515be | |||
e2bce7e7a1 | |||
9d0cc13b65 | |||
26c854a39c |
16 changed files with 560 additions and 76 deletions
|
@ -28,7 +28,7 @@ _ZRM_ provides a simple interface to relational databases in Zig. Define your re
|
||||||
|
|
||||||
## Versions
|
## Versions
|
||||||
|
|
||||||
ZRM 0.1.1 is made and tested with zig 0.13.0.
|
ZRM 0.2.0 is made and tested with zig 0.13.0.
|
||||||
|
|
||||||
## Work in progress
|
## Work in progress
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ ZRM aims to handle a lot for you, but it takes time to make. Have a look to [the
|
||||||
In your project directory:
|
In your project directory:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ zig fetch --save https://code.zeptotech.net/zedd/zrm/archive/v0.1.1.tar.gz
|
$ zig fetch --save https://code.zeptotech.net/zedd/zrm/archive/v0.2.0.tar.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
In `build.zig`:
|
In `build.zig`:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
.{
|
.{
|
||||||
.name = "zrm",
|
.name = "zrm",
|
||||||
.version = "0.1.1",
|
.version = "0.2.0",
|
||||||
|
|
||||||
.minimum_zig_version = "0.13.0",
|
.minimum_zig_version = "0.13.0",
|
||||||
|
|
||||||
|
|
109
src/database.zig
Normal file
109
src/database.zig
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const pg = @import("pg");
|
||||||
|
const session = @import("session.zig");
|
||||||
|
|
||||||
|
/// Abstract connection, provided by a connector.
|
||||||
|
pub const Connection = struct {
|
||||||
|
/// Raw connection.
|
||||||
|
connection: *pg.Conn,
|
||||||
|
|
||||||
|
/// Connection implementation.
|
||||||
|
_interface: struct {
|
||||||
|
instance: *anyopaque,
|
||||||
|
release: *const fn (self: *Connection) void,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Release the connection.
|
||||||
|
pub fn release(self: *Connection) void {
|
||||||
|
self._interface.release(self);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Database connection manager for queries.
|
||||||
|
pub const Connector = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
/// Internal interface structure.
|
||||||
|
_interface: struct {
|
||||||
|
instance: *anyopaque,
|
||||||
|
getConnection: *const fn (self: *anyopaque) anyerror!*Connection,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Get a connection.
|
||||||
|
pub fn getConnection(self: Self) !*Connection {
|
||||||
|
return try self._interface.getConnection(self._interface.instance);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// A simple pool connection.
|
||||||
|
pub const PoolConnection = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
/// Connector of the connection.
|
||||||
|
connector: *PoolConnector,
|
||||||
|
/// Connection instance, to only keep one at a time.
|
||||||
|
_connection: ?Connection = null,
|
||||||
|
|
||||||
|
/// Get a database connection.
|
||||||
|
pub fn connection(self: *Self) !*Connection {
|
||||||
|
if (self._connection == null) {
|
||||||
|
// A new connection needs to be initialized.
|
||||||
|
self._connection = .{
|
||||||
|
.connection = try self.connector.pool.acquire(),
|
||||||
|
._interface = .{
|
||||||
|
.instance = self,
|
||||||
|
.release = releaseConnection,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the initialized connection.
|
||||||
|
return &(self._connection.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implementation.
|
||||||
|
|
||||||
|
/// Release the pool connection.
|
||||||
|
fn releaseConnection(self: *Connection) void {
|
||||||
|
self.connection.release();
|
||||||
|
|
||||||
|
// Free allocated connection.
|
||||||
|
const poolConnection: *PoolConnection = @ptrCast(@alignCast(self._interface.instance));
|
||||||
|
poolConnection.connector.pool._allocator.destroy(poolConnection);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A simple pool connector.
|
||||||
|
pub const PoolConnector = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pool: *pg.Pool,
|
||||||
|
|
||||||
|
/// Get a database connector instance for the current pool.
|
||||||
|
pub fn connector(self: *Self) Connector {
|
||||||
|
return .{
|
||||||
|
._interface = .{
|
||||||
|
.instance = self,
|
||||||
|
.getConnection = getConnection,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implementation.
|
||||||
|
|
||||||
|
/// Get the connection from the pool.
|
||||||
|
fn getConnection(opaqueSelf: *anyopaque) !*Connection {
|
||||||
|
const self: *Self = @ptrCast(@alignCast(opaqueSelf));
|
||||||
|
|
||||||
|
// Initialize a new connection.
|
||||||
|
const poolConnection = try self.pool._allocator.create(PoolConnection);
|
||||||
|
poolConnection.* = .{
|
||||||
|
.connector = self,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Acquire a new connection from the pool.
|
||||||
|
return try poolConnection.connection();
|
||||||
|
}
|
||||||
|
};
|
29
src/helpers.zig
Normal file
29
src/helpers.zig
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
/// Simple ModelFromSql and ModelToSql functions for models which have the same table definition.
|
||||||
|
pub fn TableModel(comptime Model: type, comptime TableShape: type) type {
|
||||||
|
// Get fields of the model, which must be the same as the table shape.
|
||||||
|
const fields = std.meta.fields(Model);
|
||||||
|
|
||||||
|
return struct {
|
||||||
|
/// Simply copy all fields from model to table.
|
||||||
|
pub fn copyModelToTable(_model: Model) !TableShape {
|
||||||
|
var _table: TableShape = undefined;
|
||||||
|
inline for (fields) |modelField| {
|
||||||
|
// Copy each field of the model to the table.
|
||||||
|
@field(_table, modelField.name) = @field(_model, modelField.name);
|
||||||
|
}
|
||||||
|
return _table;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simply copy all fields from table to model.
|
||||||
|
pub fn copyTableToModel(_table: TableShape) !Model {
|
||||||
|
var _model: Model = undefined;
|
||||||
|
inline for (fields) |tableField| {
|
||||||
|
// Copy each field of the table to the model.
|
||||||
|
@field(_model, tableField.name) = @field(_table, tableField.name);
|
||||||
|
}
|
||||||
|
return _model;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -2,22 +2,53 @@ const std = @import("std");
|
||||||
const pg = @import("pg");
|
const pg = @import("pg");
|
||||||
const zollections = @import("zollections");
|
const zollections = @import("zollections");
|
||||||
const errors = @import("errors.zig");
|
const errors = @import("errors.zig");
|
||||||
|
const database = @import("database.zig");
|
||||||
const postgresql = @import("postgresql.zig");
|
const postgresql = @import("postgresql.zig");
|
||||||
const _sql = @import("sql.zig");
|
const _sql = @import("sql.zig");
|
||||||
const repository = @import("repository.zig");
|
const repository = @import("repository.zig");
|
||||||
|
|
||||||
/// Type of an insertable column. Insert shape should be composed of only these.
|
/// Type of an insertable column. Insert shape should be composed of only these.
|
||||||
pub fn Insertable(comptime ValueType: type) type {
|
fn InsertableColumn(comptime ValueType: type) type {
|
||||||
return struct {
|
return struct {
|
||||||
value: ?ValueType = null,
|
value: ?ValueType = null,
|
||||||
default: bool = false,
|
default: bool = false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build an insertable structure type from a normal structure.
|
||||||
|
pub fn Insertable(comptime StructType: type) type {
|
||||||
|
// Get type info of the given structure.
|
||||||
|
const typeInfo = @typeInfo(StructType);
|
||||||
|
|
||||||
|
// Initialize fields of the insertable struct.
|
||||||
|
var newFields: [typeInfo.Struct.fields.len]std.builtin.Type.StructField = undefined;
|
||||||
|
for (typeInfo.Struct.fields, &newFields) |field, *newField| {
|
||||||
|
// Create a new field for each field of the given struct.
|
||||||
|
const newFieldType = InsertableColumn(field.type);
|
||||||
|
newField.* = std.builtin.Type.StructField{
|
||||||
|
.name = field.name,
|
||||||
|
.type = newFieldType,
|
||||||
|
.default_value = null,
|
||||||
|
.is_comptime = false,
|
||||||
|
.alignment = @alignOf(newFieldType),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the insertable structure type.
|
||||||
|
return @Type(std.builtin.Type{
|
||||||
|
.Struct = .{
|
||||||
|
.layout = .auto,
|
||||||
|
.decls = &[0]std.builtin.Type.Declaration{},
|
||||||
|
.fields = &newFields,
|
||||||
|
.is_tuple = false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Repository insert query configuration structure.
|
/// Repository insert query configuration structure.
|
||||||
pub fn RepositoryInsertConfiguration(comptime InsertShape: type) type {
|
pub fn RepositoryInsertConfiguration(comptime InsertShape: type) type {
|
||||||
return struct {
|
return struct {
|
||||||
values: []const InsertShape = undefined,
|
values: []const Insertable(InsertShape) = undefined,
|
||||||
returning: ?_sql.SqlParams = null,
|
returning: ?_sql.SqlParams = null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -88,13 +119,14 @@ pub fn RepositoryInsert(comptime Model: type, comptime TableShape: type, comptim
|
||||||
const Configuration = RepositoryInsertConfiguration(InsertShape);
|
const Configuration = RepositoryInsertConfiguration(InsertShape);
|
||||||
|
|
||||||
arena: std.heap.ArenaAllocator,
|
arena: std.heap.ArenaAllocator,
|
||||||
database: *pg.Pool,
|
connector: database.Connector,
|
||||||
|
connection: *database.Connection = undefined,
|
||||||
insertConfig: Configuration,
|
insertConfig: Configuration,
|
||||||
|
|
||||||
sql: ?[]const u8 = null,
|
sql: ?[]const u8 = null,
|
||||||
|
|
||||||
/// Parse given model or shape and put the result in newValue.
|
/// Parse given model or shape and put the result in newValue.
|
||||||
fn parseData(newValue: *InsertShape, value: anytype) !void {
|
fn parseData(newValue: *Insertable(InsertShape), value: anytype) !void {
|
||||||
// If the given value is a model, first convert it to its SQL equivalent.
|
// If the given value is a model, first convert it to its SQL equivalent.
|
||||||
if (@TypeOf(value) == Model) {
|
if (@TypeOf(value) == Model) {
|
||||||
return parseData(newValue, try repositoryConfig.toSql(value));
|
return parseData(newValue, try repositoryConfig.toSql(value));
|
||||||
|
@ -107,7 +139,7 @@ pub fn RepositoryInsert(comptime Model: type, comptime TableShape: type, comptim
|
||||||
|
|
||||||
/// Parse one value to insert.
|
/// Parse one value to insert.
|
||||||
fn parseOne(self: *Self, value: anytype) !void {
|
fn parseOne(self: *Self, value: anytype) !void {
|
||||||
const newValues = try self.arena.allocator().alloc(InsertShape, 1);
|
const newValues = try self.arena.allocator().alloc(Insertable(InsertShape), 1);
|
||||||
// Parse the given value.
|
// Parse the given value.
|
||||||
try parseData(&newValues[0], value);
|
try parseData(&newValues[0], value);
|
||||||
self.insertConfig.values = newValues;
|
self.insertConfig.values = newValues;
|
||||||
|
@ -115,7 +147,7 @@ pub fn RepositoryInsert(comptime Model: type, comptime TableShape: type, comptim
|
||||||
|
|
||||||
/// Parse a slice of values to insert.
|
/// Parse a slice of values to insert.
|
||||||
fn parseSlice(self: *Self, value: anytype) !void {
|
fn parseSlice(self: *Self, value: anytype) !void {
|
||||||
const newValues = try self.arena.allocator().alloc(InsertShape, value.len);
|
const newValues = try self.arena.allocator().alloc(Insertable(InsertShape), value.len);
|
||||||
for (0..value.len) |i| {
|
for (0..value.len) |i| {
|
||||||
// Parse each value in the given slice.
|
// Parse each value in the given slice.
|
||||||
try parseData(&newValues[i], value[i]);
|
try parseData(&newValues[i], value[i]);
|
||||||
|
@ -259,20 +291,19 @@ pub fn RepositoryInsert(comptime Model: type, comptime TableShape: type, comptim
|
||||||
/// Execute the insert query.
|
/// Execute the insert query.
|
||||||
fn execQuery(self: *Self) !*pg.Result {
|
fn execQuery(self: *Self) !*pg.Result {
|
||||||
// Get a connection to the database.
|
// Get a connection to the database.
|
||||||
const connection = try self.database.acquire();
|
self.connection = try self.connector.getConnection();
|
||||||
errdefer connection.release();
|
errdefer self.connection.release();
|
||||||
|
|
||||||
// Initialize a new PostgreSQL statement.
|
// Initialize a new PostgreSQL statement.
|
||||||
var statement = try pg.Stmt.init(connection, .{
|
var statement = try pg.Stmt.init(self.connection.connection, .{
|
||||||
.column_names = true,
|
.column_names = true,
|
||||||
.release_conn = true,
|
|
||||||
.allocator = self.arena.allocator(),
|
.allocator = self.arena.allocator(),
|
||||||
});
|
});
|
||||||
errdefer statement.deinit();
|
errdefer statement.deinit();
|
||||||
|
|
||||||
// Prepare SQL insert query.
|
// Prepare SQL insert query.
|
||||||
statement.prepare(self.sql.?)
|
statement.prepare(self.sql.?)
|
||||||
catch |err| return postgresql.handlePostgresqlError(err, connection, &statement);
|
catch |err| return postgresql.handlePostgresqlError(err, self.connection, &statement);
|
||||||
|
|
||||||
// Bind INSERT query parameters.
|
// Bind INSERT query parameters.
|
||||||
for (self.insertConfig.values) |row| {
|
for (self.insertConfig.values) |row| {
|
||||||
|
@ -287,7 +318,7 @@ pub fn RepositoryInsert(comptime Model: type, comptime TableShape: type, comptim
|
||||||
|
|
||||||
// Execute the query and get its result.
|
// Execute the query and get its result.
|
||||||
const result = statement.execute()
|
const result = statement.execute()
|
||||||
catch |err| return postgresql.handlePostgresqlError(err, connection, &statement);
|
catch |err| return postgresql.handlePostgresqlError(err, self.connection, &statement);
|
||||||
|
|
||||||
// Query executed successfully, return the result.
|
// Query executed successfully, return the result.
|
||||||
return result;
|
return result;
|
||||||
|
@ -300,6 +331,7 @@ pub fn RepositoryInsert(comptime Model: type, comptime TableShape: type, comptim
|
||||||
|
|
||||||
// Execute query and get its result.
|
// Execute query and get its result.
|
||||||
var queryResult = try self.execQuery();
|
var queryResult = try self.execQuery();
|
||||||
|
defer self.connection.release();
|
||||||
defer queryResult.deinit();
|
defer queryResult.deinit();
|
||||||
|
|
||||||
//TODO deduplicate this in postgresql.zig, we could do it if Mapper type was exposed.
|
//TODO deduplicate this in postgresql.zig, we could do it if Mapper type was exposed.
|
||||||
|
@ -329,11 +361,11 @@ pub fn RepositoryInsert(comptime Model: type, comptime TableShape: type, comptim
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize a new repository insert query.
|
/// Initialize a new repository insert query.
|
||||||
pub fn init(allocator: std.mem.Allocator, database: *pg.Pool) Self {
|
pub fn init(allocator: std.mem.Allocator, connector: database.Connector) Self {
|
||||||
return .{
|
return .{
|
||||||
// Initialize an arena allocator for the insert query.
|
// Initialize an arena allocator for the insert query.
|
||||||
.arena = std.heap.ArenaAllocator.init(allocator),
|
.arena = std.heap.ArenaAllocator.init(allocator),
|
||||||
.database = database,
|
.connector = connector,
|
||||||
.insertConfig = .{},
|
.insertConfig = .{},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ const std = @import("std");
|
||||||
const pg = @import("pg");
|
const pg = @import("pg");
|
||||||
const global = @import("global.zig");
|
const global = @import("global.zig");
|
||||||
const errors = @import("errors.zig");
|
const errors = @import("errors.zig");
|
||||||
|
const database = @import("database.zig");
|
||||||
const _sql = @import("sql.zig");
|
const _sql = @import("sql.zig");
|
||||||
const repository = @import("repository.zig");
|
const repository = @import("repository.zig");
|
||||||
|
|
||||||
|
@ -31,11 +32,16 @@ pub fn bindQueryParameter(statement: *pg.Stmt, parameter: _sql.QueryParameter) !
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PostgreSQL error handling by ZRM.
|
/// PostgreSQL error handling by ZRM.
|
||||||
pub fn handlePostgresqlError(err: anyerror, connection: *pg.Conn, statement: *pg.Stmt) anyerror {
|
pub fn handlePostgresqlError(err: anyerror, connection: *database.Connection, statement: *pg.Stmt) anyerror {
|
||||||
// Release connection and statement as query failed.
|
// Release connection and statement as query failed.
|
||||||
defer statement.deinit();
|
defer statement.deinit();
|
||||||
defer connection.release();
|
defer connection.release();
|
||||||
|
|
||||||
|
return handleRawPostgresqlError(err, connection.connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PostgreSQL raw error handling by ZRM.
|
||||||
|
pub fn handleRawPostgresqlError(err: anyerror, connection: *pg.Conn) anyerror {
|
||||||
if (connection.err) |sqlErr| {
|
if (connection.err) |sqlErr| {
|
||||||
if (global.debugMode) {
|
if (global.debugMode) {
|
||||||
// If debug mode is enabled, show the PostgreSQL error.
|
// If debug mode is enabled, show the PostgreSQL error.
|
||||||
|
|
|
@ -2,6 +2,7 @@ const std = @import("std");
|
||||||
const pg = @import("pg");
|
const pg = @import("pg");
|
||||||
const zollections = @import("zollections");
|
const zollections = @import("zollections");
|
||||||
const errors = @import("errors.zig");
|
const errors = @import("errors.zig");
|
||||||
|
const database = @import("database.zig");
|
||||||
const postgresql = @import("postgresql.zig");
|
const postgresql = @import("postgresql.zig");
|
||||||
const _sql = @import("sql.zig");
|
const _sql = @import("sql.zig");
|
||||||
const conditions = @import("conditions.zig");
|
const conditions = @import("conditions.zig");
|
||||||
|
@ -29,7 +30,8 @@ pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
arena: std.heap.ArenaAllocator,
|
arena: std.heap.ArenaAllocator,
|
||||||
database: *pg.Pool,
|
connector: database.Connector,
|
||||||
|
connection: *database.Connection = undefined,
|
||||||
queryConfig: RepositoryQueryConfiguration,
|
queryConfig: RepositoryQueryConfiguration,
|
||||||
|
|
||||||
sql: ?[]const u8 = null,
|
sql: ?[]const u8 = null,
|
||||||
|
@ -176,23 +178,21 @@ pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute the built query.
|
/// Execute the built query.
|
||||||
fn execQuery(self: *Self) !*pg.Result
|
fn execQuery(self: *Self) !*pg.Result {
|
||||||
{
|
// Get the connection to the database.
|
||||||
// Get a connection to the database.
|
self.connection = try self.connector.getConnection();
|
||||||
const connection = try self.database.acquire();
|
errdefer self.connection.release();
|
||||||
errdefer connection.release();
|
|
||||||
|
|
||||||
// Initialize a new PostgreSQL statement.
|
// Initialize a new PostgreSQL statement.
|
||||||
var statement = try pg.Stmt.init(connection, .{
|
var statement = try pg.Stmt.init(self.connection.connection, .{
|
||||||
.column_names = true,
|
.column_names = true,
|
||||||
.release_conn = true,
|
|
||||||
.allocator = self.arena.allocator(),
|
.allocator = self.arena.allocator(),
|
||||||
});
|
});
|
||||||
errdefer statement.deinit();
|
errdefer statement.deinit();
|
||||||
|
|
||||||
// Prepare SQL query.
|
// Prepare SQL query.
|
||||||
statement.prepare(self.sql.?)
|
statement.prepare(self.sql.?)
|
||||||
catch |err| return postgresql.handlePostgresqlError(err, connection, &statement);
|
catch |err| return postgresql.handlePostgresqlError(err, self.connection, &statement);
|
||||||
|
|
||||||
// Bind query parameters.
|
// Bind query parameters.
|
||||||
if (self.queryConfig.select) |_select|
|
if (self.queryConfig.select) |_select|
|
||||||
|
@ -204,7 +204,7 @@ pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime
|
||||||
|
|
||||||
// Execute the query and get its result.
|
// Execute the query and get its result.
|
||||||
const result = statement.execute()
|
const result = statement.execute()
|
||||||
catch |err| return postgresql.handlePostgresqlError(err, connection, &statement);
|
catch |err| return postgresql.handlePostgresqlError(err, self.connection, &statement);
|
||||||
|
|
||||||
// Query executed successfully, return the result.
|
// Query executed successfully, return the result.
|
||||||
return result;
|
return result;
|
||||||
|
@ -217,6 +217,7 @@ pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime
|
||||||
|
|
||||||
// Execute query and get its result.
|
// Execute query and get its result.
|
||||||
var queryResult = try self.execQuery();
|
var queryResult = try self.execQuery();
|
||||||
|
defer self.connection.release();
|
||||||
defer queryResult.deinit();
|
defer queryResult.deinit();
|
||||||
|
|
||||||
//TODO deduplicate this in postgresql.zig, we could do it if Mapper type was exposed.
|
//TODO deduplicate this in postgresql.zig, we could do it if Mapper type was exposed.
|
||||||
|
@ -246,17 +247,18 @@ pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize a new repository query.
|
/// Initialize a new repository query.
|
||||||
pub fn init(allocator: std.mem.Allocator, database: *pg.Pool, queryConfig: RepositoryQueryConfiguration) Self {
|
pub fn init(allocator: std.mem.Allocator, connector: database.Connector, queryConfig: RepositoryQueryConfiguration) Self {
|
||||||
return .{
|
return .{
|
||||||
// Initialize the query arena allocator.
|
// Initialize the query arena allocator.
|
||||||
.arena = std.heap.ArenaAllocator.init(allocator),
|
.arena = std.heap.ArenaAllocator.init(allocator),
|
||||||
.database = database,
|
.connector = connector,
|
||||||
.queryConfig = queryConfig,
|
.queryConfig = queryConfig,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deinitialize the repository query.
|
/// Deinitialize the repository query.
|
||||||
pub fn deinit(self: *Self) void {
|
pub fn deinit(self: *Self) void {
|
||||||
|
// Free everything allocated for this query.
|
||||||
self.arena.deinit();
|
self.arena.deinit();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const pg = @import("pg");
|
|
||||||
const zollections = @import("zollections");
|
const zollections = @import("zollections");
|
||||||
|
const database = @import("database.zig");
|
||||||
const _sql = @import("sql.zig");
|
const _sql = @import("sql.zig");
|
||||||
const _conditions = @import("conditions.zig");
|
const _conditions = @import("conditions.zig");
|
||||||
const query = @import("query.zig");
|
const query = @import("query.zig");
|
||||||
|
@ -50,12 +50,15 @@ pub fn ModelKeyType(comptime Model: type, comptime TableShape: type, comptime co
|
||||||
var fieldName: [keyName.len:0]u8 = undefined;
|
var fieldName: [keyName.len:0]u8 = undefined;
|
||||||
@memcpy(fieldName[0..keyName.len], keyName);
|
@memcpy(fieldName[0..keyName.len], keyName);
|
||||||
|
|
||||||
|
// Get current field type.
|
||||||
|
const fieldType = std.meta.fields(TableShape)[std.meta.fieldIndex(TableShape, keyName).?].type;
|
||||||
|
|
||||||
field.* = .{
|
field.* = .{
|
||||||
.name = &fieldName,
|
.name = &fieldName,
|
||||||
.type = std.meta.fields(TableShape)[std.meta.fieldIndex(TableShape, keyName).?].type,
|
.type = fieldType,
|
||||||
.default_value = null,
|
.default_value = null,
|
||||||
.is_comptime = false,
|
.is_comptime = false,
|
||||||
.alignment = 0,
|
.alignment = @alignOf(fieldType),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,9 +97,9 @@ pub fn Repository(comptime Model: type, comptime TableShape: type, comptime conf
|
||||||
/// modelKey can be an array / slice of keys.
|
/// modelKey can be an array / slice of keys.
|
||||||
/// For composite keys: modelKey must be a struct with all the keys, matching the type of their corresponding field.
|
/// For composite keys: modelKey must be a struct with all the keys, matching the type of their corresponding field.
|
||||||
/// modelKey can be an array / slice of these structs.
|
/// modelKey can be an array / slice of these structs.
|
||||||
pub fn find(allocator: std.mem.Allocator, database: *pg.Pool, modelKey: anytype) !RepositoryResult(Model) {
|
pub fn find(allocator: std.mem.Allocator, connector: database.Connector, modelKey: anytype) !RepositoryResult(Model) {
|
||||||
// Initialize a new query.
|
// Initialize a new query.
|
||||||
var modelQuery = Self.Query.init(allocator, database, .{});
|
var modelQuery = Self.Query.init(allocator, connector, .{});
|
||||||
defer modelQuery.deinit();
|
defer modelQuery.deinit();
|
||||||
|
|
||||||
if (config.key.len == 1) {
|
if (config.key.len == 1) {
|
||||||
|
@ -111,13 +114,27 @@ pub fn Repository(comptime Model: type, comptime TableShape: type, comptime conf
|
||||||
.One => {
|
.One => {
|
||||||
switch (@typeInfo(ptr.child)) {
|
switch (@typeInfo(ptr.child)) {
|
||||||
// Add a whereIn with the array.
|
// Add a whereIn with the array.
|
||||||
.Array => try modelQuery.whereIn(keyType, keyName, modelKey),
|
.Array => {
|
||||||
|
if (ptr.child == u8)
|
||||||
|
// If the child is a string, use it as a simple value.
|
||||||
|
try modelQuery.whereValue(KeyType, keyName, "=", modelKey)
|
||||||
|
else
|
||||||
|
// Otherwise, use it as an array.
|
||||||
|
try modelQuery.whereIn(keyType, keyName, modelKey);
|
||||||
|
},
|
||||||
// Add a simple condition with the pointed value.
|
// Add a simple condition with the pointed value.
|
||||||
else => try modelQuery.whereValue(keyType, keyName, "=", modelKey.*),
|
else => try modelQuery.whereValue(keyType, keyName, "=", modelKey.*),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Add a whereIn with the slice.
|
// Add a whereIn with the slice.
|
||||||
else => try modelQuery.whereIn(keyType, keyName, modelKey),
|
else => {
|
||||||
|
if (ptr.child == u8)
|
||||||
|
// If the child is a string, use it as a simple value.
|
||||||
|
try modelQuery.whereValue(KeyType, keyName, "=", modelKey)
|
||||||
|
else
|
||||||
|
// Otherwise, use it as an array.
|
||||||
|
try modelQuery.whereIn(keyType, keyName, modelKey);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Add a simple condition with the given value.
|
// Add a simple condition with the given value.
|
||||||
|
@ -162,9 +179,9 @@ pub fn Repository(comptime Model: type, comptime TableShape: type, comptime conf
|
||||||
|
|
||||||
/// Perform creation of the given new model in the repository.
|
/// Perform creation of the given new model in the repository.
|
||||||
/// The model will be altered with the inserted values.
|
/// The model will be altered with the inserted values.
|
||||||
pub fn create(allocator: std.mem.Allocator, database: *pg.Pool, newModel: *Model) !RepositoryResult(Model) {
|
pub fn create(allocator: std.mem.Allocator, connector: database.Connector, newModel: *Model) !RepositoryResult(Model) {
|
||||||
// Initialize a new insert query for the given model.
|
// Initialize a new insert query for the given model.
|
||||||
var insertQuery = Self.Insert.init(allocator, database);
|
var insertQuery = Self.Insert.init(allocator, connector);
|
||||||
defer insertQuery.deinit();
|
defer insertQuery.deinit();
|
||||||
try insertQuery.values(newModel);
|
try insertQuery.values(newModel);
|
||||||
insertQuery.returningAll();
|
insertQuery.returningAll();
|
||||||
|
@ -182,12 +199,12 @@ pub fn Repository(comptime Model: type, comptime TableShape: type, comptime conf
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform save of the given existing model in the repository.
|
/// Perform save of the given existing model in the repository.
|
||||||
pub fn save(allocator: std.mem.Allocator, database: *pg.Pool, existingModel: *Model) !RepositoryResult(Model) {
|
pub fn save(allocator: std.mem.Allocator, connector: database.Connector, existingModel: *Model) !RepositoryResult(Model) {
|
||||||
// Convert the model to its SQL form.
|
// Convert the model to its SQL form.
|
||||||
const modelSql = try config.toSql(existingModel.*);
|
const modelSql = try config.toSql(existingModel.*);
|
||||||
|
|
||||||
// Initialize a new update query for the given model.
|
// Initialize a new update query for the given model.
|
||||||
var updateQuery = Self.Update(TableShape).init(allocator, database);
|
var updateQuery = Self.Update(TableShape).init(allocator, connector);
|
||||||
defer updateQuery.deinit();
|
defer updateQuery.deinit();
|
||||||
try updateQuery.set(modelSql);
|
try updateQuery.set(modelSql);
|
||||||
updateQuery.returningAll();
|
updateQuery.returningAll();
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const global = @import("global.zig");
|
const global = @import("global.zig");
|
||||||
|
const session = @import("session.zig");
|
||||||
const repository = @import("repository.zig");
|
const repository = @import("repository.zig");
|
||||||
const insert = @import("insert.zig");
|
const insert = @import("insert.zig");
|
||||||
const _sql = @import("sql.zig");
|
const _sql = @import("sql.zig");
|
||||||
|
@ -14,6 +15,11 @@ pub const Insertable = insert.Insertable;
|
||||||
pub const QueryParameter = _sql.QueryParameter;
|
pub const QueryParameter = _sql.QueryParameter;
|
||||||
pub const SqlParams = _sql.SqlParams;
|
pub const SqlParams = _sql.SqlParams;
|
||||||
|
|
||||||
|
pub const database = @import("database.zig");
|
||||||
|
pub const Session = session.Session;
|
||||||
|
|
||||||
pub const conditions = @import("conditions.zig");
|
pub const conditions = @import("conditions.zig");
|
||||||
|
|
||||||
pub const errors = @import("errors.zig");
|
pub const errors = @import("errors.zig");
|
||||||
|
|
||||||
|
pub const helpers = @import("helpers.zig");
|
||||||
|
|
122
src/session.zig
Normal file
122
src/session.zig
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const pg = @import("pg");
|
||||||
|
const postgresql = @import("postgresql.zig");
|
||||||
|
const database = @import("database.zig");
|
||||||
|
|
||||||
|
/// Session for multiple repository operations.
|
||||||
|
pub const Session = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
_database: *pg.Pool,
|
||||||
|
|
||||||
|
/// The active connection for the session.
|
||||||
|
connection: *pg.Conn,
|
||||||
|
|
||||||
|
/// Execute a comptime-known SQL command for the current session.
|
||||||
|
fn exec(self: Self, comptime sql: []const u8) !void {
|
||||||
|
_ = self.connection.exec(sql, .{}) catch |err| {
|
||||||
|
return postgresql.handleRawPostgresqlError(err, self.connection);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Begin a new transaction.
|
||||||
|
pub fn beginTransaction(self: Self) !void {
|
||||||
|
try self.exec("BEGIN;");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rollback the current transaction.
|
||||||
|
pub fn rollbackTransaction(self: Self) !void {
|
||||||
|
try self.exec("ROLLBACK;");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Commit the current transaction.
|
||||||
|
pub fn commitTransaction(self: Self) !void {
|
||||||
|
try self.exec("COMMIT;");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new savepoint with the given name.
|
||||||
|
pub fn savepoint(self: Self, comptime _savepoint: []const u8) !void {
|
||||||
|
try self.exec("SAVEPOINT " ++ _savepoint ++ ";");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rollback to the savepoint with the given name.
|
||||||
|
pub fn rollbackTo(self: Self, comptime _savepoint: []const u8) !void {
|
||||||
|
try self.exec("ROLLBACK TO " ++ _savepoint ++ ";");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize a new session.
|
||||||
|
pub fn init(_database: *pg.Pool) !Session {
|
||||||
|
return .{
|
||||||
|
._database = _database,
|
||||||
|
.connection = try _database.acquire(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deinitialize the session.
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.connection.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a database connector instance for the current session.
|
||||||
|
pub fn connector(self: *Self) database.Connector {
|
||||||
|
return database.Connector{
|
||||||
|
._interface = .{
|
||||||
|
.instance = self,
|
||||||
|
.getConnection = getConnection,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connector implementation.
|
||||||
|
|
||||||
|
/// Get the current connection.
|
||||||
|
fn getConnection(opaqueSelf: *anyopaque) !*database.Connection {
|
||||||
|
const self: *Self = @ptrCast(@alignCast(opaqueSelf));
|
||||||
|
|
||||||
|
// Initialize a new connection.
|
||||||
|
const sessionConnection = try self._database._allocator.create(SessionConnection);
|
||||||
|
sessionConnection.* = .{
|
||||||
|
.session = self,
|
||||||
|
};
|
||||||
|
|
||||||
|
return try sessionConnection.connection();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn noRelease(_: *anyopaque) void {}
|
||||||
|
|
||||||
|
/// A session connection.
|
||||||
|
const SessionConnection = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
/// Session of the connection.
|
||||||
|
session: *Session,
|
||||||
|
|
||||||
|
/// Connection instance, to only keep one at a time.
|
||||||
|
_connection: ?database.Connection = null,
|
||||||
|
|
||||||
|
/// Get a database connection.
|
||||||
|
pub fn connection(self: *Self) !*database.Connection {
|
||||||
|
if (self._connection == null) {
|
||||||
|
// A new connection needs to be initialized.
|
||||||
|
self._connection = .{
|
||||||
|
.connection = self.session.connection,
|
||||||
|
._interface = .{
|
||||||
|
.instance = self,
|
||||||
|
.release = releaseConnection,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return &(self._connection.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implementation.
|
||||||
|
|
||||||
|
/// Free the current connection (doesn't actually release the connection, as it is required to stay the same all along the session).
|
||||||
|
fn releaseConnection(self: *database.Connection) void {
|
||||||
|
// Free allocated connection.
|
||||||
|
const sessionConnection: *SessionConnection = @ptrCast(@alignCast(self._interface.instance));
|
||||||
|
sessionConnection.session._database._allocator.destroy(sessionConnection);
|
||||||
|
}
|
||||||
|
};
|
|
@ -2,6 +2,7 @@ const std = @import("std");
|
||||||
const pg = @import("pg");
|
const pg = @import("pg");
|
||||||
const zollections = @import("zollections");
|
const zollections = @import("zollections");
|
||||||
const errors = @import("errors.zig");
|
const errors = @import("errors.zig");
|
||||||
|
const database = @import("database.zig");
|
||||||
const postgresql = @import("postgresql.zig");
|
const postgresql = @import("postgresql.zig");
|
||||||
const _sql = @import("sql.zig");
|
const _sql = @import("sql.zig");
|
||||||
const conditions = @import("conditions.zig");
|
const conditions = @import("conditions.zig");
|
||||||
|
@ -57,7 +58,8 @@ pub fn RepositoryUpdate(comptime Model: type, comptime TableShape: type, comptim
|
||||||
const Configuration = RepositoryUpdateConfiguration(UpdateShape);
|
const Configuration = RepositoryUpdateConfiguration(UpdateShape);
|
||||||
|
|
||||||
arena: std.heap.ArenaAllocator,
|
arena: std.heap.ArenaAllocator,
|
||||||
database: *pg.Pool,
|
connector: database.Connector,
|
||||||
|
connection: *database.Connection = undefined,
|
||||||
updateConfig: Configuration,
|
updateConfig: Configuration,
|
||||||
|
|
||||||
sql: ?[]const u8 = null,
|
sql: ?[]const u8 = null,
|
||||||
|
@ -261,20 +263,19 @@ pub fn RepositoryUpdate(comptime Model: type, comptime TableShape: type, comptim
|
||||||
/// Execute the update query.
|
/// Execute the update query.
|
||||||
fn execQuery(self: *Self) !*pg.Result {
|
fn execQuery(self: *Self) !*pg.Result {
|
||||||
// Get a connection to the database.
|
// Get a connection to the database.
|
||||||
const connection = try self.database.acquire();
|
self.connection = try self.connector.getConnection();
|
||||||
errdefer connection.release();
|
errdefer self.connection.release();
|
||||||
|
|
||||||
// Initialize a new PostgreSQL statement.
|
// Initialize a new PostgreSQL statement.
|
||||||
var statement = try pg.Stmt.init(connection, .{
|
var statement = try pg.Stmt.init(self.connection.connection, .{
|
||||||
.column_names = true,
|
.column_names = true,
|
||||||
.release_conn = true,
|
|
||||||
.allocator = self.arena.allocator(),
|
.allocator = self.arena.allocator(),
|
||||||
});
|
});
|
||||||
errdefer statement.deinit();
|
errdefer statement.deinit();
|
||||||
|
|
||||||
// Prepare SQL update query.
|
// Prepare SQL update query.
|
||||||
statement.prepare(self.sql.?)
|
statement.prepare(self.sql.?)
|
||||||
catch |err| return postgresql.handlePostgresqlError(err, connection, &statement);
|
catch |err| return postgresql.handlePostgresqlError(err, self.connection, &statement);
|
||||||
|
|
||||||
// Bind UPDATE query parameters.
|
// Bind UPDATE query parameters.
|
||||||
inline for (columns) |column| {
|
inline for (columns) |column| {
|
||||||
|
@ -291,7 +292,7 @@ pub fn RepositoryUpdate(comptime Model: type, comptime TableShape: type, comptim
|
||||||
|
|
||||||
// Execute the query and get its result.
|
// Execute the query and get its result.
|
||||||
const result = statement.execute()
|
const result = statement.execute()
|
||||||
catch |err| return postgresql.handlePostgresqlError(err, connection, &statement);
|
catch |err| return postgresql.handlePostgresqlError(err, self.connection, &statement);
|
||||||
|
|
||||||
// Query executed successfully, return the result.
|
// Query executed successfully, return the result.
|
||||||
return result;
|
return result;
|
||||||
|
@ -304,6 +305,7 @@ pub fn RepositoryUpdate(comptime Model: type, comptime TableShape: type, comptim
|
||||||
|
|
||||||
// Execute query and get its result.
|
// Execute query and get its result.
|
||||||
var queryResult = try self.execQuery();
|
var queryResult = try self.execQuery();
|
||||||
|
defer self.connection.release();
|
||||||
defer queryResult.deinit();
|
defer queryResult.deinit();
|
||||||
|
|
||||||
//TODO deduplicate this in postgresql.zig, we could do it if Mapper type was exposed.
|
//TODO deduplicate this in postgresql.zig, we could do it if Mapper type was exposed.
|
||||||
|
@ -333,11 +335,11 @@ pub fn RepositoryUpdate(comptime Model: type, comptime TableShape: type, comptim
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize a new repository update query.
|
/// Initialize a new repository update query.
|
||||||
pub fn init(allocator: std.mem.Allocator, database: *pg.Pool) Self {
|
pub fn init(allocator: std.mem.Allocator, connector: database.Connector) Self {
|
||||||
return .{
|
return .{
|
||||||
// Initialize an arena allocator for the update query.
|
// Initialize an arena allocator for the update query.
|
||||||
.arena = std.heap.ArenaAllocator.init(allocator),
|
.arena = std.heap.ArenaAllocator.init(allocator),
|
||||||
.database = database,
|
.connector = connector,
|
||||||
.updateConfig = .{},
|
.updateConfig = .{},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ fn initDatabase() !void {
|
||||||
.password = "zrm",
|
.password = "zrm",
|
||||||
.database = "zrm",
|
.database = "zrm",
|
||||||
},
|
},
|
||||||
|
.size = 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,14 +61,14 @@ const CompositeModelRepository = zrm.Repository(CompositeModel, CompositeModelTa
|
||||||
|
|
||||||
// Insert shape used by default for inserts in the repository.
|
// Insert shape used by default for inserts in the repository.
|
||||||
.insertShape = struct {
|
.insertShape = struct {
|
||||||
secondcol: zrm.Insertable([]const u8),
|
secondcol: []const u8,
|
||||||
label: zrm.Insertable([]const u8),
|
label: []const u8,
|
||||||
},
|
},
|
||||||
|
|
||||||
.key = &[_][]const u8{"firstcol", "secondcol"},
|
.key = &[_][]const u8{"firstcol", "secondcol"},
|
||||||
|
|
||||||
.fromSql = &modelFromSql,
|
.fromSql = &zrm.helpers.TableModel(CompositeModel, CompositeModelTable).copyTableToModel,
|
||||||
.toSql = &modelToSql,
|
.toSql = &zrm.helpers.TableModel(CompositeModel, CompositeModelTable).copyModelToTable,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,6 +77,9 @@ test "composite model create, save and find" {
|
||||||
|
|
||||||
try initDatabase();
|
try initDatabase();
|
||||||
defer database.deinit();
|
defer database.deinit();
|
||||||
|
var poolConnector = zrm.database.PoolConnector{
|
||||||
|
.pool = database,
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize a test model.
|
// Initialize a test model.
|
||||||
var newModel = CompositeModel{
|
var newModel = CompositeModel{
|
||||||
|
@ -86,7 +90,7 @@ test "composite model create, save and find" {
|
||||||
|
|
||||||
|
|
||||||
// Create the new model.
|
// Create the new model.
|
||||||
var result = try CompositeModelRepository.create(std.testing.allocator, database, &newModel);
|
var result = try CompositeModelRepository.create(std.testing.allocator, poolConnector.connector(), &newModel);
|
||||||
defer result.deinit(); // Will clear some values in newModel.
|
defer result.deinit(); // Will clear some values in newModel.
|
||||||
|
|
||||||
// Check that the model is correctly defined.
|
// Check that the model is correctly defined.
|
||||||
|
@ -101,7 +105,7 @@ test "composite model create, save and find" {
|
||||||
// Update the model.
|
// Update the model.
|
||||||
newModel.label = null;
|
newModel.label = null;
|
||||||
|
|
||||||
var result2 = try CompositeModelRepository.save(std.testing.allocator, database, &newModel);
|
var result2 = try CompositeModelRepository.save(std.testing.allocator, poolConnector.connector(), &newModel);
|
||||||
defer result2.deinit(); // Will clear some values in newModel.
|
defer result2.deinit(); // Will clear some values in newModel.
|
||||||
|
|
||||||
// Checking that the model has been updated (but only the right field).
|
// Checking that the model has been updated (but only the right field).
|
||||||
|
@ -111,7 +115,7 @@ test "composite model create, save and find" {
|
||||||
|
|
||||||
|
|
||||||
// Do another insert with the same secondcol.
|
// Do another insert with the same secondcol.
|
||||||
var insertQuery = CompositeModelRepository.Insert.init(std.testing.allocator, database);
|
var insertQuery = CompositeModelRepository.Insert.init(std.testing.allocator, poolConnector.connector());
|
||||||
defer insertQuery.deinit();
|
defer insertQuery.deinit();
|
||||||
try insertQuery.values(.{
|
try insertQuery.values(.{
|
||||||
.secondcol = "identifier",
|
.secondcol = "identifier",
|
||||||
|
@ -128,7 +132,7 @@ test "composite model create, save and find" {
|
||||||
|
|
||||||
|
|
||||||
// Try to find the created then saved model, to check that everything has been saved correctly.
|
// 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, .{
|
var result4 = try CompositeModelRepository.find(std.testing.allocator, poolConnector.connector(), .{
|
||||||
.firstcol = newModel.firstcol,
|
.firstcol = newModel.firstcol,
|
||||||
.secondcol = newModel.secondcol,
|
.secondcol = newModel.secondcol,
|
||||||
});
|
});
|
||||||
|
@ -139,7 +143,7 @@ test "composite model create, save and find" {
|
||||||
|
|
||||||
|
|
||||||
// Try to find multiple models at once.
|
// Try to find multiple models at once.
|
||||||
var result5 = try CompositeModelRepository.find(std.testing.allocator, database, &[_]CompositeModelRepository.KeyType{
|
var result5 = try CompositeModelRepository.find(std.testing.allocator, poolConnector.connector(), &[_]CompositeModelRepository.KeyType{
|
||||||
.{
|
.{
|
||||||
.firstcol = newModel.firstcol,
|
.firstcol = newModel.firstcol,
|
||||||
.secondcol = newModel.secondcol,
|
.secondcol = newModel.secondcol,
|
||||||
|
|
|
@ -11,6 +11,7 @@ CREATE TABLE models (
|
||||||
|
|
||||||
-- Insert default data.
|
-- Insert default data.
|
||||||
INSERT INTO models(name, amount) VALUES ('test', 50);
|
INSERT INTO models(name, amount) VALUES ('test', 50);
|
||||||
|
INSERT INTO models(name, amount) VALUES ('updatable', 33.12);
|
||||||
|
|
||||||
-- Create default composite models table.
|
-- Create default composite models table.
|
||||||
CREATE TABLE composite_models (
|
CREATE TABLE composite_models (
|
||||||
|
|
|
@ -17,6 +17,7 @@ fn initDatabase() !void {
|
||||||
.password = "zrm",
|
.password = "zrm",
|
||||||
.database = "zrm",
|
.database = "zrm",
|
||||||
},
|
},
|
||||||
|
.size = 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +30,7 @@ const MySubmodel = struct {
|
||||||
};
|
};
|
||||||
|
|
||||||
/// An example model.
|
/// An example model.
|
||||||
const MyModel = struct {
|
pub const MyModel = struct {
|
||||||
id: i32,
|
id: i32,
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
amount: f64,
|
amount: f64,
|
||||||
|
@ -38,7 +39,7 @@ const MyModel = struct {
|
||||||
};
|
};
|
||||||
|
|
||||||
/// SQL table shape of the example model.
|
/// SQL table shape of the example model.
|
||||||
const MyModelTable = struct {
|
pub const MyModelTable = struct {
|
||||||
id: i32,
|
id: i32,
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
amount: f64,
|
amount: f64,
|
||||||
|
@ -63,13 +64,13 @@ fn modelToSql(model: MyModel) !MyModelTable {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Declare a model repository.
|
/// Declare a model repository.
|
||||||
const MyModelRepository = zrm.Repository(MyModel, MyModelTable, .{
|
pub const MyModelRepository = zrm.Repository(MyModel, MyModelTable, .{
|
||||||
.table = "models",
|
.table = "models",
|
||||||
|
|
||||||
// Insert shape used by default for inserts in the repository.
|
// Insert shape used by default for inserts in the repository.
|
||||||
.insertShape = struct {
|
.insertShape = struct {
|
||||||
name: zrm.Insertable([]const u8),
|
name: []const u8,
|
||||||
amount: zrm.Insertable(f64),
|
amount: f64,
|
||||||
},
|
},
|
||||||
|
|
||||||
.key = &[_][]const u8{"id"},
|
.key = &[_][]const u8{"id"},
|
||||||
|
@ -105,8 +106,11 @@ test "repository query SQL builder" {
|
||||||
|
|
||||||
try initDatabase();
|
try initDatabase();
|
||||||
defer database.deinit();
|
defer database.deinit();
|
||||||
|
var poolConnector = zrm.database.PoolConnector{
|
||||||
|
.pool = database,
|
||||||
|
};
|
||||||
|
|
||||||
var query = MyModelRepository.Query.init(std.testing.allocator, database, .{});
|
var query = MyModelRepository.Query.init(std.testing.allocator, poolConnector.connector(), .{});
|
||||||
defer query.deinit();
|
defer query.deinit();
|
||||||
try query.whereIn(usize, "id", &[_]usize{1, 2});
|
try query.whereIn(usize, "id", &[_]usize{1, 2});
|
||||||
try query.buildSql();
|
try query.buildSql();
|
||||||
|
@ -121,9 +125,12 @@ test "repository element retrieval" {
|
||||||
|
|
||||||
try initDatabase();
|
try initDatabase();
|
||||||
defer database.deinit();
|
defer database.deinit();
|
||||||
|
var poolConnector = zrm.database.PoolConnector{
|
||||||
|
.pool = database,
|
||||||
|
};
|
||||||
|
|
||||||
// Prepare a query for models.
|
// Prepare a query for models.
|
||||||
var query = MyModelRepository.Query.init(std.testing.allocator, database, .{});
|
var query = MyModelRepository.Query.init(std.testing.allocator, poolConnector.connector(), .{});
|
||||||
try query.whereValue(usize, "id", "=", 1);
|
try query.whereValue(usize, "id", "=", 1);
|
||||||
defer query.deinit();
|
defer query.deinit();
|
||||||
|
|
||||||
|
@ -152,8 +159,11 @@ test "repository complex SQL query" {
|
||||||
|
|
||||||
try initDatabase();
|
try initDatabase();
|
||||||
defer database.deinit();
|
defer database.deinit();
|
||||||
|
var poolConnector = zrm.database.PoolConnector{
|
||||||
|
.pool = database,
|
||||||
|
};
|
||||||
|
|
||||||
var query = MyModelRepository.Query.init(std.testing.allocator, database, .{});
|
var query = MyModelRepository.Query.init(std.testing.allocator, poolConnector.connector(), .{});
|
||||||
defer query.deinit();
|
defer query.deinit();
|
||||||
query.where(
|
query.where(
|
||||||
try query.newCondition().@"or"(&[_]zrm.SqlParams{
|
try query.newCondition().@"or"(&[_]zrm.SqlParams{
|
||||||
|
@ -187,6 +197,9 @@ test "repository element creation" {
|
||||||
|
|
||||||
try initDatabase();
|
try initDatabase();
|
||||||
defer database.deinit();
|
defer database.deinit();
|
||||||
|
var poolConnector = zrm.database.PoolConnector{
|
||||||
|
.pool = database,
|
||||||
|
};
|
||||||
|
|
||||||
// Create a model to insert.
|
// Create a model to insert.
|
||||||
const newModel = MyModel{
|
const newModel = MyModel{
|
||||||
|
@ -196,7 +209,7 @@ test "repository element creation" {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize an insert query.
|
// Initialize an insert query.
|
||||||
var insertQuery = MyModelRepository.Insert.init(std.testing.allocator, database);
|
var insertQuery = MyModelRepository.Insert.init(std.testing.allocator, poolConnector.connector());
|
||||||
defer insertQuery.deinit();
|
defer insertQuery.deinit();
|
||||||
// Insert the new model.
|
// Insert the new model.
|
||||||
try insertQuery.values(newModel);
|
try insertQuery.values(newModel);
|
||||||
|
@ -225,16 +238,19 @@ test "repository element update" {
|
||||||
|
|
||||||
try initDatabase();
|
try initDatabase();
|
||||||
defer database.deinit();
|
defer database.deinit();
|
||||||
|
var poolConnector = zrm.database.PoolConnector{
|
||||||
|
.pool = database,
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize an update query.
|
// Initialize an update query.
|
||||||
var updateQuery = MyModelRepository.Update(struct {
|
var updateQuery = MyModelRepository.Update(struct {
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
}).init(std.testing.allocator, database);
|
}).init(std.testing.allocator, poolConnector.connector());
|
||||||
defer updateQuery.deinit();
|
defer updateQuery.deinit();
|
||||||
|
|
||||||
// Update a model's name.
|
// Update a model's name.
|
||||||
try updateQuery.set(.{ .name = "newname" });
|
try updateQuery.set(.{ .name = "newname" });
|
||||||
try updateQuery.whereValue(usize, "id", "=", 1);
|
try updateQuery.whereValue(usize, "id", "=", 2);
|
||||||
updateQuery.returningAll();
|
updateQuery.returningAll();
|
||||||
|
|
||||||
// Build SQL.
|
// Build SQL.
|
||||||
|
@ -251,7 +267,7 @@ test "repository element update" {
|
||||||
|
|
||||||
// Check the updated model.
|
// Check the updated model.
|
||||||
try std.testing.expectEqual(1, result.models.len);
|
try std.testing.expectEqual(1, result.models.len);
|
||||||
try std.testing.expectEqual(1, result.models[0].id);
|
try std.testing.expectEqual(2, result.models[0].id);
|
||||||
try std.testing.expectEqualStrings("newname", result.models[0].name);
|
try std.testing.expectEqualStrings("newname", result.models[0].name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -260,6 +276,9 @@ test "model create, save and find" {
|
||||||
|
|
||||||
try initDatabase();
|
try initDatabase();
|
||||||
defer database.deinit();
|
defer database.deinit();
|
||||||
|
var poolConnector = zrm.database.PoolConnector{
|
||||||
|
.pool = database,
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize a test model.
|
// Initialize a test model.
|
||||||
var newModel = MyModel{
|
var newModel = MyModel{
|
||||||
|
@ -270,7 +289,7 @@ test "model create, save and find" {
|
||||||
|
|
||||||
|
|
||||||
// Create the new model.
|
// Create the new model.
|
||||||
var result = try MyModelRepository.create(std.testing.allocator, database, &newModel);
|
var result = try MyModelRepository.create(std.testing.allocator, poolConnector.connector(), &newModel);
|
||||||
defer result.deinit(); // Will clear some values in newModel.
|
defer result.deinit(); // Will clear some values in newModel.
|
||||||
|
|
||||||
// Check that the model is correctly defined.
|
// Check that the model is correctly defined.
|
||||||
|
@ -284,7 +303,7 @@ test "model create, save and find" {
|
||||||
// Update the model.
|
// Update the model.
|
||||||
newModel.name = "recently updated name";
|
newModel.name = "recently updated name";
|
||||||
|
|
||||||
var result2 = try MyModelRepository.save(std.testing.allocator, database, &newModel);
|
var result2 = try MyModelRepository.save(std.testing.allocator, poolConnector.connector(), &newModel);
|
||||||
defer result2.deinit(); // Will clear some values in newModel.
|
defer result2.deinit(); // Will clear some values in newModel.
|
||||||
|
|
||||||
// Checking that the model has been updated (but only the right field).
|
// Checking that the model has been updated (but only the right field).
|
||||||
|
@ -296,7 +315,7 @@ test "model create, save and find" {
|
||||||
// Do another update.
|
// Do another update.
|
||||||
newModel.amount = 12.226;
|
newModel.amount = 12.226;
|
||||||
|
|
||||||
var result3 = try MyModelRepository.save(std.testing.allocator, database, &newModel);
|
var result3 = try MyModelRepository.save(std.testing.allocator, poolConnector.connector(), &newModel);
|
||||||
defer result3.deinit(); // Will clear some values in newModel.
|
defer result3.deinit(); // Will clear some values in newModel.
|
||||||
|
|
||||||
// Checking that the model has been updated (but only the right field).
|
// Checking that the model has been updated (but only the right field).
|
||||||
|
@ -306,14 +325,14 @@ test "model create, save and find" {
|
||||||
|
|
||||||
|
|
||||||
// Try to find the created then saved model, to check that everything has been saved correctly.
|
// 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);
|
var result4 = try MyModelRepository.find(std.testing.allocator, poolConnector.connector(), newModel.id);
|
||||||
defer result4.deinit(); // Will clear some values in newModel.
|
defer result4.deinit(); // Will clear some values in newModel.
|
||||||
|
|
||||||
try std.testing.expectEqualDeep(newModel, result4.first().?.*);
|
try std.testing.expectEqualDeep(newModel, result4.first().?.*);
|
||||||
|
|
||||||
|
|
||||||
// Try to find multiple models at once.
|
// Try to find multiple models at once.
|
||||||
var result5 = try MyModelRepository.find(std.testing.allocator, database, &[_]i32{1, newModel.id});
|
var result5 = try MyModelRepository.find(std.testing.allocator, poolConnector.connector(), &[_]i32{1, newModel.id});
|
||||||
defer result5.deinit();
|
defer result5.deinit();
|
||||||
|
|
||||||
try std.testing.expectEqual(2, result5.models.len);
|
try std.testing.expectEqual(2, result5.models.len);
|
||||||
|
|
|
@ -4,4 +4,5 @@ comptime {
|
||||||
_ = @import("query.zig");
|
_ = @import("query.zig");
|
||||||
_ = @import("repository.zig");
|
_ = @import("repository.zig");
|
||||||
_ = @import("composite.zig");
|
_ = @import("composite.zig");
|
||||||
|
_ = @import("sessions.zig");
|
||||||
}
|
}
|
||||||
|
|
134
tests/sessions.zig
Normal file
134
tests/sessions.zig
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const pg = @import("pg");
|
||||||
|
const zrm = @import("zrm");
|
||||||
|
const repository = @import("repository.zig");
|
||||||
|
|
||||||
|
/// PostgreSQL database connection.
|
||||||
|
var database: *pg.Pool = undefined;
|
||||||
|
|
||||||
|
/// Initialize database connection.
|
||||||
|
fn initDatabase(allocator: std.mem.Allocator) !void {
|
||||||
|
database = try pg.Pool.init(allocator, .{
|
||||||
|
.connect = .{
|
||||||
|
.host = "localhost",
|
||||||
|
.port = 5432,
|
||||||
|
},
|
||||||
|
.auth = .{
|
||||||
|
.username = "zrm",
|
||||||
|
.password = "zrm",
|
||||||
|
.database = "zrm",
|
||||||
|
},
|
||||||
|
.size = 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test "session with rolled back transaction and savepoint" {
|
||||||
|
zrm.setDebug(true);
|
||||||
|
|
||||||
|
// Initialize database.
|
||||||
|
try initDatabase(std.testing.allocator);
|
||||||
|
defer database.deinit();
|
||||||
|
|
||||||
|
// Start a new session and perform operations in a transaction.
|
||||||
|
var session = try zrm.Session.init(database);
|
||||||
|
defer session.deinit();
|
||||||
|
|
||||||
|
|
||||||
|
try session.beginTransaction();
|
||||||
|
|
||||||
|
|
||||||
|
// First UPDATE in the transaction.
|
||||||
|
{
|
||||||
|
var firstUpdate = repository.MyModelRepository.Update(struct {
|
||||||
|
name: []const u8,
|
||||||
|
}).init(std.testing.allocator, session.connector());
|
||||||
|
defer firstUpdate.deinit();
|
||||||
|
try firstUpdate.set(.{
|
||||||
|
.name = "tempname",
|
||||||
|
});
|
||||||
|
try firstUpdate.whereValue(usize, "id", "=", 1);
|
||||||
|
var firstUpdateResult = try firstUpdate.update(std.testing.allocator);
|
||||||
|
firstUpdateResult.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Set a savepoint.
|
||||||
|
try session.savepoint("my_savepoint");
|
||||||
|
|
||||||
|
|
||||||
|
// Second UPDATE in the transaction.
|
||||||
|
{
|
||||||
|
var secondUpdate = repository.MyModelRepository.Update(struct {
|
||||||
|
amount: f64,
|
||||||
|
}).init(std.testing.allocator, session.connector());
|
||||||
|
defer secondUpdate.deinit();
|
||||||
|
try secondUpdate.set(.{
|
||||||
|
.amount = 52.25,
|
||||||
|
});
|
||||||
|
try secondUpdate.whereValue(usize, "id", "=", 1);
|
||||||
|
var secondUpdateResult = try secondUpdate.update(std.testing.allocator);
|
||||||
|
secondUpdateResult.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// SELECT before rollback to savepoint in the transaction.
|
||||||
|
{
|
||||||
|
var queryBeforeRollbackToSavepoint = repository.MyModelRepository.Query.init(std.testing.allocator, session.connector(), .{});
|
||||||
|
try queryBeforeRollbackToSavepoint.whereValue(usize, "id", "=", 1);
|
||||||
|
defer queryBeforeRollbackToSavepoint.deinit();
|
||||||
|
|
||||||
|
// Get models.
|
||||||
|
var resultBeforeRollbackToSavepoint = try queryBeforeRollbackToSavepoint.get(std.testing.allocator);
|
||||||
|
defer resultBeforeRollbackToSavepoint.deinit();
|
||||||
|
|
||||||
|
// Check that one model has been retrieved, then check its type and values.
|
||||||
|
try std.testing.expectEqual(1, resultBeforeRollbackToSavepoint.models.len);
|
||||||
|
try std.testing.expectEqual(repository.MyModel, @TypeOf(resultBeforeRollbackToSavepoint.models[0].*));
|
||||||
|
try std.testing.expectEqual(1, resultBeforeRollbackToSavepoint.models[0].id);
|
||||||
|
try std.testing.expectEqualStrings("tempname", resultBeforeRollbackToSavepoint.models[0].name);
|
||||||
|
try std.testing.expectEqual(52.25, resultBeforeRollbackToSavepoint.models[0].amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
try session.rollbackTo("my_savepoint");
|
||||||
|
|
||||||
|
|
||||||
|
// SELECT after rollback to savepoint in the transaction.
|
||||||
|
{
|
||||||
|
var queryAfterRollbackToSavepoint = repository.MyModelRepository.Query.init(std.testing.allocator, session.connector(), .{});
|
||||||
|
try queryAfterRollbackToSavepoint.whereValue(usize, "id", "=", 1);
|
||||||
|
defer queryAfterRollbackToSavepoint.deinit();
|
||||||
|
|
||||||
|
// Get models.
|
||||||
|
var resultAfterRollbackToSavepoint = try queryAfterRollbackToSavepoint.get(std.testing.allocator);
|
||||||
|
defer resultAfterRollbackToSavepoint.deinit();
|
||||||
|
|
||||||
|
// Check that one model has been retrieved, then check its type and values.
|
||||||
|
try std.testing.expectEqual(1, resultAfterRollbackToSavepoint.models.len);
|
||||||
|
try std.testing.expectEqual(repository.MyModel, @TypeOf(resultAfterRollbackToSavepoint.models[0].*));
|
||||||
|
try std.testing.expectEqual(1, resultAfterRollbackToSavepoint.models[0].id);
|
||||||
|
try std.testing.expectEqualStrings("tempname", resultAfterRollbackToSavepoint.models[0].name);
|
||||||
|
try std.testing.expectEqual(50.00, resultAfterRollbackToSavepoint.models[0].amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
try session.rollbackTransaction();
|
||||||
|
|
||||||
|
|
||||||
|
// SELECT outside of the rolled back transaction.
|
||||||
|
{
|
||||||
|
var queryOutside = repository.MyModelRepository.Query.init(std.testing.allocator, session.connector(), .{});
|
||||||
|
try queryOutside.whereValue(usize, "id", "=", 1);
|
||||||
|
defer queryOutside.deinit();
|
||||||
|
|
||||||
|
// Get models.
|
||||||
|
var resultOutside = try queryOutside.get(std.testing.allocator);
|
||||||
|
defer resultOutside.deinit();
|
||||||
|
|
||||||
|
// Check that one model has been retrieved, then check its type and values.
|
||||||
|
try std.testing.expectEqual(1, resultOutside.models.len);
|
||||||
|
try std.testing.expectEqual(repository.MyModel, @TypeOf(resultOutside.models[0].*));
|
||||||
|
try std.testing.expectEqual(1, resultOutside.models[0].id);
|
||||||
|
try std.testing.expectEqualStrings("test", resultOutside.models[0].name);
|
||||||
|
try std.testing.expectEqual(50.00, resultOutside.models[0].amount);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue