Compare commits

..

No commits in common. "main" and "v0.1.1" have entirely different histories.
main ... v0.1.1

16 changed files with 76 additions and 560 deletions

View file

@ -28,7 +28,7 @@ _ZRM_ provides a simple interface to relational databases in Zig. Define your re
## Versions ## Versions
ZRM 0.2.0 is made and tested with zig 0.13.0. ZRM 0.1.1 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.2.0.tar.gz $ zig fetch --save https://code.zeptotech.net/zedd/zrm/archive/v0.1.1.tar.gz
``` ```
In `build.zig`: In `build.zig`:

View file

@ -1,6 +1,6 @@
.{ .{
.name = "zrm", .name = "zrm",
.version = "0.2.0", .version = "0.1.1",
.minimum_zig_version = "0.13.0", .minimum_zig_version = "0.13.0",

View file

@ -1,109 +0,0 @@
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();
}
};

View file

@ -1,29 +0,0 @@
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;
}
};
}

View file

@ -2,53 +2,22 @@ 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.
fn InsertableColumn(comptime ValueType: type) type { pub fn Insertable(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 Insertable(InsertShape) = undefined, values: []const InsertShape = undefined,
returning: ?_sql.SqlParams = null, returning: ?_sql.SqlParams = null,
}; };
} }
@ -119,14 +88,13 @@ 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,
connector: database.Connector, database: *pg.Pool,
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: *Insertable(InsertShape), value: anytype) !void { fn parseData(newValue: *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));
@ -139,7 +107,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(Insertable(InsertShape), 1); const newValues = try self.arena.allocator().alloc(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;
@ -147,7 +115,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(Insertable(InsertShape), value.len); const newValues = try self.arena.allocator().alloc(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]);
@ -291,19 +259,20 @@ 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.
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(self.connection.connection, .{ var statement = try pg.Stmt.init(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, self.connection, &statement); catch |err| return postgresql.handlePostgresqlError(err, connection, &statement);
// Bind INSERT query parameters. // Bind INSERT query parameters.
for (self.insertConfig.values) |row| { for (self.insertConfig.values) |row| {
@ -318,7 +287,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, self.connection, &statement); catch |err| return postgresql.handlePostgresqlError(err, connection, &statement);
// Query executed successfully, return the result. // Query executed successfully, return the result.
return result; return result;
@ -331,7 +300,6 @@ 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.
@ -361,11 +329,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, connector: database.Connector) Self { pub fn init(allocator: std.mem.Allocator, database: *pg.Pool) 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),
.connector = connector, .database = database,
.insertConfig = .{}, .insertConfig = .{},
}; };
} }

View file

@ -2,7 +2,6 @@ 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");
@ -32,16 +31,11 @@ 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: *database.Connection, statement: *pg.Stmt) anyerror { pub fn handlePostgresqlError(err: anyerror, connection: *pg.Conn, 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.

View file

@ -2,7 +2,6 @@ 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");
@ -30,8 +29,7 @@ pub fn RepositoryQuery(comptime Model: type, comptime TableShape: type, comptime
const Self = @This(); const Self = @This();
arena: std.heap.ArenaAllocator, arena: std.heap.ArenaAllocator,
connector: database.Connector, database: *pg.Pool,
connection: *database.Connection = undefined,
queryConfig: RepositoryQueryConfiguration, queryConfig: RepositoryQueryConfiguration,
sql: ?[]const u8 = null, sql: ?[]const u8 = null,
@ -178,21 +176,23 @@ 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. {
self.connection = try self.connector.getConnection(); // Get a connection to the database.
errdefer self.connection.release(); const connection = try self.database.acquire();
errdefer connection.release();
// Initialize a new PostgreSQL statement. // Initialize a new PostgreSQL statement.
var statement = try pg.Stmt.init(self.connection.connection, .{ var statement = try pg.Stmt.init(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, self.connection, &statement); catch |err| return postgresql.handlePostgresqlError(err, 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, self.connection, &statement); catch |err| return postgresql.handlePostgresqlError(err, connection, &statement);
// Query executed successfully, return the result. // Query executed successfully, return the result.
return result; return result;
@ -217,7 +217,6 @@ 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.
@ -247,18 +246,17 @@ 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, connector: database.Connector, queryConfig: RepositoryQueryConfiguration) Self { pub fn init(allocator: std.mem.Allocator, database: *pg.Pool, 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),
.connector = connector, .database = database,
.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();
} }
}; };

View file

@ -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,15 +50,12 @@ 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 = fieldType, .type = std.meta.fields(TableShape)[std.meta.fieldIndex(TableShape, keyName).?].type,
.default_value = null, .default_value = null,
.is_comptime = false, .is_comptime = false,
.alignment = @alignOf(fieldType), .alignment = 0,
}; };
} }
@ -97,9 +94,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, connector: database.Connector, modelKey: anytype) !RepositoryResult(Model) { pub fn find(allocator: std.mem.Allocator, database: *pg.Pool, modelKey: anytype) !RepositoryResult(Model) {
// Initialize a new query. // Initialize a new query.
var modelQuery = Self.Query.init(allocator, connector, .{}); var modelQuery = Self.Query.init(allocator, database, .{});
defer modelQuery.deinit(); defer modelQuery.deinit();
if (config.key.len == 1) { if (config.key.len == 1) {
@ -114,27 +111,13 @@ 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 => { .Array => try modelQuery.whereIn(keyType, keyName, modelKey),
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 => { else => try modelQuery.whereIn(keyType, keyName, modelKey),
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.
@ -179,9 +162,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, connector: database.Connector, newModel: *Model) !RepositoryResult(Model) { pub fn create(allocator: std.mem.Allocator, database: *pg.Pool, 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, connector); var insertQuery = Self.Insert.init(allocator, database);
defer insertQuery.deinit(); defer insertQuery.deinit();
try insertQuery.values(newModel); try insertQuery.values(newModel);
insertQuery.returningAll(); insertQuery.returningAll();
@ -199,12 +182,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, connector: database.Connector, existingModel: *Model) !RepositoryResult(Model) { pub fn save(allocator: std.mem.Allocator, database: *pg.Pool, 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, connector); var updateQuery = Self.Update(TableShape).init(allocator, database);
defer updateQuery.deinit(); defer updateQuery.deinit();
try updateQuery.set(modelSql); try updateQuery.set(modelSql);
updateQuery.returningAll(); updateQuery.returningAll();

View file

@ -1,5 +1,4 @@
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");
@ -15,11 +14,6 @@ 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");

View file

@ -1,122 +0,0 @@
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);
}
};

View file

@ -2,7 +2,6 @@ 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");
@ -58,8 +57,7 @@ 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,
connector: database.Connector, database: *pg.Pool,
connection: *database.Connection = undefined,
updateConfig: Configuration, updateConfig: Configuration,
sql: ?[]const u8 = null, sql: ?[]const u8 = null,
@ -263,19 +261,20 @@ 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.
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(self.connection.connection, .{ var statement = try pg.Stmt.init(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, self.connection, &statement); catch |err| return postgresql.handlePostgresqlError(err, connection, &statement);
// Bind UPDATE query parameters. // Bind UPDATE query parameters.
inline for (columns) |column| { inline for (columns) |column| {
@ -292,7 +291,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, self.connection, &statement); catch |err| return postgresql.handlePostgresqlError(err, connection, &statement);
// Query executed successfully, return the result. // Query executed successfully, return the result.
return result; return result;
@ -305,7 +304,6 @@ 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.
@ -335,11 +333,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, connector: database.Connector) Self { pub fn init(allocator: std.mem.Allocator, database: *pg.Pool) 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),
.connector = connector, .database = database,
.updateConfig = .{}, .updateConfig = .{},
}; };
} }

View file

@ -17,7 +17,6 @@ fn initDatabase() !void {
.password = "zrm", .password = "zrm",
.database = "zrm", .database = "zrm",
}, },
.size = 1,
}); });
} }
@ -61,14 +60,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: []const u8, secondcol: zrm.Insertable([]const u8),
label: []const u8, label: zrm.Insertable([]const u8),
}, },
.key = &[_][]const u8{"firstcol", "secondcol"}, .key = &[_][]const u8{"firstcol", "secondcol"},
.fromSql = &zrm.helpers.TableModel(CompositeModel, CompositeModelTable).copyTableToModel, .fromSql = &modelFromSql,
.toSql = &zrm.helpers.TableModel(CompositeModel, CompositeModelTable).copyModelToTable, .toSql = &modelToSql,
}); });
@ -77,9 +76,6 @@ 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{
@ -90,7 +86,7 @@ test "composite model create, save and find" {
// Create the new model. // Create the new model.
var result = try CompositeModelRepository.create(std.testing.allocator, poolConnector.connector(), &newModel); var result = try CompositeModelRepository.create(std.testing.allocator, database, &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.
@ -105,7 +101,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, poolConnector.connector(), &newModel); var result2 = try CompositeModelRepository.save(std.testing.allocator, database, &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).
@ -115,7 +111,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, poolConnector.connector()); var insertQuery = CompositeModelRepository.Insert.init(std.testing.allocator, database);
defer insertQuery.deinit(); defer insertQuery.deinit();
try insertQuery.values(.{ try insertQuery.values(.{
.secondcol = "identifier", .secondcol = "identifier",
@ -132,7 +128,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, poolConnector.connector(), .{ var result4 = try CompositeModelRepository.find(std.testing.allocator, database, .{
.firstcol = newModel.firstcol, .firstcol = newModel.firstcol,
.secondcol = newModel.secondcol, .secondcol = newModel.secondcol,
}); });
@ -143,7 +139,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, poolConnector.connector(), &[_]CompositeModelRepository.KeyType{ var result5 = try CompositeModelRepository.find(std.testing.allocator, database, &[_]CompositeModelRepository.KeyType{
.{ .{
.firstcol = newModel.firstcol, .firstcol = newModel.firstcol,
.secondcol = newModel.secondcol, .secondcol = newModel.secondcol,

View file

@ -11,7 +11,6 @@ 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 (

View file

@ -17,7 +17,6 @@ fn initDatabase() !void {
.password = "zrm", .password = "zrm",
.database = "zrm", .database = "zrm",
}, },
.size = 1,
}); });
} }
@ -30,7 +29,7 @@ const MySubmodel = struct {
}; };
/// An example model. /// An example model.
pub const MyModel = struct { const MyModel = struct {
id: i32, id: i32,
name: []const u8, name: []const u8,
amount: f64, amount: f64,
@ -39,7 +38,7 @@ pub const MyModel = struct {
}; };
/// SQL table shape of the example model. /// SQL table shape of the example model.
pub const MyModelTable = struct { const MyModelTable = struct {
id: i32, id: i32,
name: []const u8, name: []const u8,
amount: f64, amount: f64,
@ -64,13 +63,13 @@ fn modelToSql(model: MyModel) !MyModelTable {
} }
/// Declare a model repository. /// Declare a model repository.
pub const MyModelRepository = zrm.Repository(MyModel, MyModelTable, .{ 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: []const u8, name: zrm.Insertable([]const u8),
amount: f64, amount: zrm.Insertable(f64),
}, },
.key = &[_][]const u8{"id"}, .key = &[_][]const u8{"id"},
@ -106,11 +105,8 @@ 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, poolConnector.connector(), .{}); var query = MyModelRepository.Query.init(std.testing.allocator, database, .{});
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();
@ -125,12 +121,9 @@ 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, poolConnector.connector(), .{}); var query = MyModelRepository.Query.init(std.testing.allocator, database, .{});
try query.whereValue(usize, "id", "=", 1); try query.whereValue(usize, "id", "=", 1);
defer query.deinit(); defer query.deinit();
@ -159,11 +152,8 @@ 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, poolConnector.connector(), .{}); var query = MyModelRepository.Query.init(std.testing.allocator, database, .{});
defer query.deinit(); defer query.deinit();
query.where( query.where(
try query.newCondition().@"or"(&[_]zrm.SqlParams{ try query.newCondition().@"or"(&[_]zrm.SqlParams{
@ -197,9 +187,6 @@ 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{
@ -209,7 +196,7 @@ test "repository element creation" {
}; };
// Initialize an insert query. // Initialize an insert query.
var insertQuery = MyModelRepository.Insert.init(std.testing.allocator, poolConnector.connector()); var insertQuery = MyModelRepository.Insert.init(std.testing.allocator, database);
defer insertQuery.deinit(); defer insertQuery.deinit();
// Insert the new model. // Insert the new model.
try insertQuery.values(newModel); try insertQuery.values(newModel);
@ -238,19 +225,16 @@ 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, poolConnector.connector()); }).init(std.testing.allocator, database);
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", "=", 2); try updateQuery.whereValue(usize, "id", "=", 1);
updateQuery.returningAll(); updateQuery.returningAll();
// Build SQL. // Build SQL.
@ -267,7 +251,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(2, result.models[0].id); try std.testing.expectEqual(1, result.models[0].id);
try std.testing.expectEqualStrings("newname", result.models[0].name); try std.testing.expectEqualStrings("newname", result.models[0].name);
} }
@ -276,9 +260,6 @@ 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{
@ -289,7 +270,7 @@ test "model create, save and find" {
// Create the new model. // Create the new model.
var result = try MyModelRepository.create(std.testing.allocator, poolConnector.connector(), &newModel); var result = try MyModelRepository.create(std.testing.allocator, database, &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.
@ -303,7 +284,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, poolConnector.connector(), &newModel); var result2 = try MyModelRepository.save(std.testing.allocator, database, &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).
@ -315,7 +296,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, poolConnector.connector(), &newModel); var result3 = try MyModelRepository.save(std.testing.allocator, database, &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).
@ -325,14 +306,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, poolConnector.connector(), newModel.id); var result4 = try MyModelRepository.find(std.testing.allocator, database, 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, poolConnector.connector(), &[_]i32{1, newModel.id}); var result5 = try MyModelRepository.find(std.testing.allocator, database, &[_]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);

View file

@ -4,5 +4,4 @@ comptime {
_ = @import("query.zig"); _ = @import("query.zig");
_ = @import("repository.zig"); _ = @import("repository.zig");
_ = @import("composite.zig"); _ = @import("composite.zig");
_ = @import("sessions.zig");
} }

View file

@ -1,134 +0,0 @@
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);
}
}