zrm/tests/repository.zig
Madeorsk f16ad46f8d
Relations and SQL query build improvements.
+ Relations definition.
+ Add test submodels in testing database.
* Generalize raw SQL query preparation and build.
* Simplify and improve SQL query build algorithms.
2024-11-22 15:40:10 +01:00

396 lines
11 KiB
Zig

const std = @import("std");
const pg = @import("pg");
const zrm = @import("zrm");
/// PostgreSQL database connection.
var database: *pg.Pool = undefined;
/// Initialize database connection.
fn initDatabase() !void {
database = try pg.Pool.init(std.testing.allocator, .{
.connect = .{
.host = "localhost",
.port = 5432,
},
.auth = .{
.username = "zrm",
.password = "zrm",
.database = "zrm",
},
.size = 1,
});
}
/// An example submodel, child of the example model.
const MySubmodel = struct {
uuid: []const u8,
label: []const u8,
parent_id: ?i32 = null,
parent: ?MyModel = null,
};
const MySubmodelTable = struct {
uuid: []const u8,
label: []const u8,
parent_id: ?i32 = null,
};
// Convert an SQL row to a model.
fn submodelFromSql(raw: MySubmodelTable) !MySubmodel {
return .{
.uuid = raw.uuid,
.label = raw.label,
.parent_id = raw.parent_id,
};
}
/// Convert a model to an SQL row.
fn submodelToSql(model: MySubmodel) !MySubmodelTable {
return .{
.uuid = model.uuid,
.label = model.label,
.parent_id = model.parent_id,
};
}
/// Declare a model repository.
pub const MySubmodelRepository = zrm.Repository(MySubmodel, MySubmodelTable, .{
.table = "submodels",
// Insert shape used by default for inserts in the repository.
.insertShape = MySubmodelTable,
.key = &[_][]const u8{"uuid"},
.fromSql = &submodelFromSql,
.toSql = &submodelToSql,
});
pub const MySubmodelRelations = MySubmodelRepository.relations.define(.{
.parent = MySubmodelRepository.relations.one(MyModelRepository, .{
.direct = .{
.foreignKey = "parent_id",
},
}),
});
/// An example model.
pub const MyModel = struct {
id: i32,
name: []const u8,
amount: f64,
submodels: ?[]const MySubmodel = null,
};
/// SQL table shape of the example model.
pub const MyModelTable = struct {
id: i32,
name: []const u8,
amount: f64,
};
// Convert an SQL row to a model.
fn modelFromSql(raw: MyModelTable) !MyModel {
return .{
.id = raw.id,
.name = raw.name,
.amount = raw.amount,
};
}
/// Convert a model to an SQL row.
fn modelToSql(model: MyModel) !MyModelTable {
return .{
.id = model.id,
.name = model.name,
.amount = model.amount,
};
}
/// Declare a model repository.
pub const MyModelRepository = zrm.Repository(MyModel, MyModelTable, .{
.table = "models",
// Insert shape used by default for inserts in the repository.
.insertShape = struct {
name: []const u8,
amount: f64,
},
.key = &[_][]const u8{"id"},
.fromSql = &modelFromSql,
.toSql = &modelToSql,
});
pub const MyModelRelations = MyModelRepository.relations.define(.{
.submodels = MyModelRepository.relations.many(MySubmodelRepository, .{
.direct = .{
.foreignKey = "parent_id",
}
}),
});
test "model structures" {
// Initialize a test model.
const testModel = MyModel{
.id = 10,
.name = "test",
.amount = 15.5,
.submodels = &[_]MySubmodel{
MySubmodel{
.uuid = "56c378bf-cfda-4438-9b33-b4c63f190907",
.label = "test",
},
},
};
// Test that the model is correctly initialized.
try std.testing.expectEqual(10, testModel.id);
try std.testing.expectEqualStrings("56c378bf-cfda-4438-9b33-b4c63f190907", testModel.submodels.?[0].uuid);
}
test "repository query SQL builder" {
zrm.setDebug(true);
try initDatabase();
defer database.deinit();
var poolConnector = zrm.database.PoolConnector{
.pool = database,
};
var query = MyModelRepository.Query.init(std.testing.allocator, poolConnector.connector(), .{});
defer query.deinit();
try query.whereIn(usize, "id", &[_]usize{1, 2});
try query.buildSql();
const expectedSql = "SELECT \"models\".* FROM \"models\" WHERE id IN ($1,$2);";
try std.testing.expectEqual(expectedSql.len, query.sql.?.len);
try std.testing.expectEqualStrings(expectedSql, query.sql.?);
}
test "repository element retrieval" {
zrm.setDebug(true);
try initDatabase();
defer database.deinit();
var poolConnector = zrm.database.PoolConnector{
.pool = database,
};
// Prepare a query for models.
var query = MyModelRepository.Query.init(std.testing.allocator, poolConnector.connector(), .{});
try query.whereValue(usize, "id", "=", 1);
defer query.deinit();
// Build SQL.
try query.buildSql();
// Check built SQL.
const expectedSql = "SELECT \"models\".* FROM \"models\" WHERE id = $1;";
try std.testing.expectEqual(expectedSql.len, query.sql.?.len);
try std.testing.expectEqualStrings(expectedSql, query.sql.?);
// Get models.
var result = try query.get(std.testing.allocator);
defer result.deinit();
// Check that one model has been retrieved, then check its type and values.
try std.testing.expectEqual(1, result.models.len);
try std.testing.expectEqual(MyModel, @TypeOf(result.models[0].*));
try std.testing.expectEqual(1, result.models[0].id);
try std.testing.expectEqualStrings("test", result.models[0].name);
try std.testing.expectEqual(50.00, result.models[0].amount);
}
test "repository complex SQL query" {
zrm.setDebug(true);
try initDatabase();
defer database.deinit();
var poolConnector = zrm.database.PoolConnector{
.pool = database,
};
var query = MyModelRepository.Query.init(std.testing.allocator, poolConnector.connector(), .{});
defer query.deinit();
query.where(
try query.newCondition().@"or"(&[_]zrm.RawQuery{
try query.newCondition().value(usize, "id", "=", 1),
try query.newCondition().@"and"(&[_]zrm.RawQuery{
try query.newCondition().in(usize, "id", &[_]usize{100000, 200000, 300000}),
try query.newCondition().@"or"(&[_]zrm.RawQuery{
try query.newCondition().value(f64, "amount", ">", 12.13),
try query.newCondition().value([]const u8, "name", "=", "test"),
})
}),
})
);
try query.buildSql();
const expectedSql = "SELECT \"models\".* FROM \"models\" WHERE (id = $1 OR (id IN ($2,$3,$4) AND (amount > $5 OR name = $6)));";
try std.testing.expectEqual(expectedSql.len, query.sql.?.len);
try std.testing.expectEqualStrings(expectedSql, query.sql.?);
// Get models.
var result = try query.get(std.testing.allocator);
defer result.deinit();
// Check that one model has been retrieved.
try std.testing.expectEqual(1, result.models.len);
try std.testing.expectEqual(1, result.models[0].id);
}
test "repository element creation" {
zrm.setDebug(true);
try initDatabase();
defer database.deinit();
var poolConnector = zrm.database.PoolConnector{
.pool = database,
};
// Create a model to insert.
const newModel = MyModel{
.id = undefined,
.amount = 75,
.name = "inserted model",
};
// Initialize an insert query.
var insertQuery = MyModelRepository.Insert.init(std.testing.allocator, poolConnector.connector());
defer insertQuery.deinit();
// Insert the new model.
try insertQuery.values(newModel);
insertQuery.returningAll();
// Build SQL.
try insertQuery.buildSql();
// Check built SQL.
const expectedSql = "INSERT INTO models(name,amount) VALUES ($1,$2) RETURNING *;";
try std.testing.expectEqual(expectedSql.len, insertQuery.sql.?.len);
try std.testing.expectEqualStrings(expectedSql, insertQuery.sql.?);
// Insert models.
var result = try insertQuery.insert(std.testing.allocator);
defer result.deinit();
// Check the inserted model.
try std.testing.expectEqual(1, result.models.len);
try std.testing.expectEqual(75, result.models[0].amount);
try std.testing.expectEqualStrings("inserted model", result.models[0].name);
}
test "repository element update" {
zrm.setDebug(true);
try initDatabase();
defer database.deinit();
var poolConnector = zrm.database.PoolConnector{
.pool = database,
};
// Initialize an update query.
var updateQuery = MyModelRepository.Update(struct {
name: []const u8,
}).init(std.testing.allocator, poolConnector.connector());
defer updateQuery.deinit();
// Update a model's name.
try updateQuery.set(.{ .name = "newname" });
try updateQuery.whereValue(usize, "id", "=", 2);
updateQuery.returningAll();
// Build SQL.
try updateQuery.buildSql();
// Check built SQL.
const expectedSql = "UPDATE models SET name=$1 WHERE id = $2 RETURNING *;";
try std.testing.expectEqual(expectedSql.len, updateQuery.sql.?.len);
try std.testing.expectEqualStrings(expectedSql, updateQuery.sql.?);
// Update models.
var result = try updateQuery.update(std.testing.allocator);
defer result.deinit();
// Check the updated model.
try std.testing.expectEqual(1, result.models.len);
try std.testing.expectEqual(2, result.models[0].id);
try std.testing.expectEqualStrings("newname", result.models[0].name);
}
test "model create, save and find" {
zrm.setDebug(true);
try initDatabase();
defer database.deinit();
var poolConnector = zrm.database.PoolConnector{
.pool = database,
};
// Initialize a test model.
var newModel = MyModel{
.id = 0,
.amount = 555,
.name = "newly created model",
};
// Create the new model.
var result = try MyModelRepository.create(std.testing.allocator, poolConnector.connector(), &newModel);
defer result.deinit(); // Will clear some values in newModel.
// Check that the model is correctly defined.
try std.testing.expect(newModel.id > 1);
try std.testing.expectEqualStrings("newly created model", newModel.name);
const postInsertId = newModel.id;
const postInsertAmount = newModel.amount;
// Update the model.
newModel.name = "recently updated name";
var result2 = try MyModelRepository.save(std.testing.allocator, poolConnector.connector(), &newModel);
defer result2.deinit(); // Will clear some values in newModel.
// Checking that the model has been updated (but only the right field).
try std.testing.expectEqual(postInsertId, newModel.id);
try std.testing.expectEqualStrings("recently updated name", newModel.name);
try std.testing.expectEqual(postInsertAmount, newModel.amount);
// Do another update.
newModel.amount = 12.226;
var result3 = try MyModelRepository.save(std.testing.allocator, poolConnector.connector(), &newModel);
defer result3.deinit(); // Will clear some values in newModel.
// Checking that the model has been updated (but only the right field).
try std.testing.expectEqual(postInsertId, newModel.id);
try std.testing.expectEqualStrings("recently updated name", newModel.name);
try std.testing.expectEqual(12.23, newModel.amount);
// Try to find the created then saved model, to check that everything has been saved correctly.
var result4 = try MyModelRepository.find(std.testing.allocator, poolConnector.connector(), newModel.id);
defer result4.deinit(); // Will clear some values in newModel.
try std.testing.expectEqualDeep(newModel, result4.first().?.*);
// Try to find multiple models at once.
var result5 = try MyModelRepository.find(std.testing.allocator, poolConnector.connector(), &[_]i32{1, newModel.id});
defer result5.deinit();
try std.testing.expectEqual(2, result5.models.len);
try std.testing.expectEqual(1, result5.models[0].id);
try std.testing.expectEqual(newModel.id, result5.models[1].id);
}