storage and config: sqlite wrapper, migrations, querylog policy, repositories, zon config with import/export/check cli
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
//! `settings`.
|
||||
//!
|
||||
//! The row type is `model.SettingPair`, the same type `model.toSettings` and
|
||||
//! `model.fromSettings` speak, so the scalar sections cross the storage boundary
|
||||
//! without a second shape.
|
||||
//!
|
||||
//! Only list / insert / deleteAll / count exist.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const db = @import("../db.zig");
|
||||
const migrations = @import("../migrations.zig");
|
||||
const model = @import("../../config/model.zig");
|
||||
const context = @import("context.zig");
|
||||
|
||||
const InsertContext = context.InsertContext;
|
||||
|
||||
/// Both strings of every pair are heap copies owned by `gpa`.
|
||||
pub fn listSettings(database: *db.Db, gpa: Allocator) db.Error!std.ArrayList(model.SettingPair) {
|
||||
var stmt = try database.prepare("SELECT key, value FROM settings ORDER BY key");
|
||||
defer stmt.deinit();
|
||||
|
||||
var out: std.ArrayList(model.SettingPair) = .empty;
|
||||
// `errdefer`s run in reverse: the free pass is declared last so it runs
|
||||
// before the backing array is released.
|
||||
errdefer out.deinit(gpa);
|
||||
errdefer freeSettings(gpa, out.items);
|
||||
|
||||
while (try stmt.step()) {
|
||||
const key = try stmt.columnTextAlloc(gpa, 0);
|
||||
errdefer gpa.free(key);
|
||||
const value = try stmt.columnTextAlloc(gpa, 1);
|
||||
errdefer gpa.free(value);
|
||||
try out.append(gpa, .{ .key = key, .value = value });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/// Only for lists `listSettings` produced. `model.toSettings` builds pairs whose
|
||||
/// `key` is a comptime string and must never be freed; that list is the caller's
|
||||
/// to release, field by field.
|
||||
pub fn freeSettings(gpa: Allocator, items: []const model.SettingPair) void {
|
||||
for (items) |item| {
|
||||
gpa.free(item.key);
|
||||
gpa.free(item.value);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insertSetting(database: *db.Db, item: model.SettingPair, ctx: InsertContext) db.Error!void {
|
||||
_ = ctx;
|
||||
var stmt = try database.prepare("INSERT INTO settings (key, value) VALUES (?1, ?2)");
|
||||
defer stmt.deinit();
|
||||
try stmt.bindText(1, item.key);
|
||||
try stmt.bindText(2, item.value);
|
||||
try stmt.exec();
|
||||
}
|
||||
|
||||
pub fn deleteAllSettings(database: *db.Db) db.Error!void {
|
||||
return database.exec("DELETE FROM settings;");
|
||||
}
|
||||
|
||||
pub fn countSettings(database: *db.Db) db.Error!i64 {
|
||||
return database.queryInt("SELECT count(*) FROM settings");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
fn openMigrated() !db.Db {
|
||||
var database = try db.Db.open(":memory:", .{ .mode = .memory });
|
||||
errdefer database.close();
|
||||
try db.applyPragmas(&database, .{});
|
||||
_ = try migrations.migrate(&database);
|
||||
return database;
|
||||
}
|
||||
|
||||
fn seedSettings(database: *db.Db) !void {
|
||||
const ctx: InsertContext = .{};
|
||||
try insertSetting(database, .{ .key = "web.port", .value = "8080" }, ctx);
|
||||
try insertSetting(database, .{ .key = "dns.port", .value = "53" }, ctx);
|
||||
// An apostrophe proves the value is bound, not concatenated into the SQL.
|
||||
try insertSetting(database, .{ .key = "logging.file_path", .value = "/var/log/o'brien.log" }, ctx);
|
||||
}
|
||||
|
||||
test "settings round-trip in ascending key order" {
|
||||
var database = try openMigrated();
|
||||
defer database.close();
|
||||
try seedSettings(&database);
|
||||
|
||||
var items = try listSettings(&database, testing.allocator);
|
||||
defer items.deinit(testing.allocator);
|
||||
defer freeSettings(testing.allocator, items.items);
|
||||
|
||||
try testing.expectEqual(@as(usize, 3), items.items.len);
|
||||
try testing.expectEqualStrings("dns.port", items.items[0].key);
|
||||
try testing.expectEqualStrings("53", items.items[0].value);
|
||||
try testing.expectEqualStrings("logging.file_path", items.items[1].key);
|
||||
try testing.expectEqualStrings("/var/log/o'brien.log", items.items[1].value);
|
||||
try testing.expectEqualStrings("web.port", items.items[2].key);
|
||||
try testing.expectEqualStrings("8080", items.items[2].value);
|
||||
}
|
||||
|
||||
test "a value holding an apostrophe survives the round trip" {
|
||||
var database = try openMigrated();
|
||||
defer database.close();
|
||||
const ctx: InsertContext = .{};
|
||||
const value = "he said 'hello'; DROP TABLE settings;--";
|
||||
try insertSetting(&database, .{ .key = "web.password_hash", .value = value }, ctx);
|
||||
|
||||
var items = try listSettings(&database, testing.allocator);
|
||||
defer items.deinit(testing.allocator);
|
||||
defer freeSettings(testing.allocator, items.items);
|
||||
|
||||
try testing.expectEqual(@as(usize, 1), items.items.len);
|
||||
try testing.expectEqualStrings(value, items.items[0].value);
|
||||
try testing.expectEqual(@as(i64, 1), try countSettings(&database));
|
||||
}
|
||||
|
||||
test "deleteAllSettings empties the table and countSettings reflects it" {
|
||||
var database = try openMigrated();
|
||||
defer database.close();
|
||||
try seedSettings(&database);
|
||||
|
||||
try testing.expectEqual(@as(i64, 3), try countSettings(&database));
|
||||
try deleteAllSettings(&database);
|
||||
try testing.expectEqual(@as(i64, 0), try countSettings(&database));
|
||||
}
|
||||
|
||||
fn listSettingsUnderFailure(gpa: Allocator) !void {
|
||||
var database = try openMigrated();
|
||||
defer database.close();
|
||||
try seedSettings(&database);
|
||||
|
||||
var items = try listSettings(&database, gpa);
|
||||
defer items.deinit(gpa);
|
||||
defer freeSettings(gpa, items.items);
|
||||
}
|
||||
|
||||
test "listSettings is leak-safe under allocation failure" {
|
||||
try testing.checkAllAllocationFailures(testing.allocator, listSettingsUnderFailure, .{});
|
||||
}
|
||||
Reference in New Issue
Block a user