storage and config: sqlite wrapper, migrations, querylog policy, repositories, zon config with import/export/check cli
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
//! `blocklist_sources`.
|
||||
//!
|
||||
//! Only the four configuration columns are read and written. `last_updated`,
|
||||
//! `domain_count`, `wildcard_count`, `skipped_regex_count` and `checksum` are
|
||||
//! facts a running server produces; an insert leaves them at their column
|
||||
//! defaults so two exports taken minutes apart stay identical.
|
||||
//!
|
||||
//! 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;
|
||||
|
||||
const list_sql =
|
||||
\\SELECT url, name, enabled, is_suggested FROM blocklist_sources ORDER BY url
|
||||
;
|
||||
|
||||
/// Every string in the result is a heap copy owned by `gpa`.
|
||||
pub fn listBlocklistSources(database: *db.Db, gpa: Allocator) db.Error!std.ArrayList(model.BlocklistSource) {
|
||||
var stmt = try database.prepare(list_sql);
|
||||
defer stmt.deinit();
|
||||
|
||||
var out: std.ArrayList(model.BlocklistSource) = .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 freeBlocklistSources(gpa, out.items);
|
||||
|
||||
while (try stmt.step()) {
|
||||
const url = try stmt.columnTextAlloc(gpa, 0);
|
||||
errdefer gpa.free(url);
|
||||
const name = try stmt.columnTextAlloc(gpa, 1);
|
||||
errdefer gpa.free(name);
|
||||
try out.append(gpa, .{
|
||||
.url = url,
|
||||
.name = name,
|
||||
.enabled = stmt.columnBool(2),
|
||||
.is_suggested = stmt.columnBool(3),
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
pub fn freeBlocklistSources(gpa: Allocator, items: []const model.BlocklistSource) void {
|
||||
for (items) |item| {
|
||||
gpa.free(item.url);
|
||||
gpa.free(item.name);
|
||||
}
|
||||
}
|
||||
|
||||
const insert_sql =
|
||||
\\INSERT INTO blocklist_sources (url, name, enabled, is_suggested) VALUES (?1, ?2, ?3, ?4)
|
||||
;
|
||||
|
||||
pub fn insertBlocklistSource(database: *db.Db, item: model.BlocklistSource, ctx: InsertContext) db.Error!void {
|
||||
_ = ctx;
|
||||
var stmt = try database.prepare(insert_sql);
|
||||
defer stmt.deinit();
|
||||
try stmt.bindText(1, item.url);
|
||||
try stmt.bindText(2, item.name);
|
||||
try stmt.bindBool(3, item.enabled);
|
||||
try stmt.bindBool(4, item.is_suggested);
|
||||
try stmt.exec();
|
||||
}
|
||||
|
||||
pub fn deleteAllBlocklistSources(database: *db.Db) db.Error!void {
|
||||
return database.exec("DELETE FROM blocklist_sources;");
|
||||
}
|
||||
|
||||
pub fn countBlocklistSources(database: *db.Db) db.Error!i64 {
|
||||
return database.queryInt("SELECT count(*) FROM blocklist_sources");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 seedSources(database: *db.Db) !void {
|
||||
const ctx: InsertContext = .{};
|
||||
try insertBlocklistSource(database, .{
|
||||
.url = "https://c.example/list.txt",
|
||||
.name = "C list",
|
||||
}, ctx);
|
||||
try insertBlocklistSource(database, .{
|
||||
.url = "https://a.example/list.txt",
|
||||
.name = "A list",
|
||||
.enabled = false,
|
||||
}, ctx);
|
||||
try insertBlocklistSource(database, .{
|
||||
.url = "https://b.example/list.txt",
|
||||
.name = "B list",
|
||||
.is_suggested = true,
|
||||
}, ctx);
|
||||
}
|
||||
|
||||
test "blocklist_sources round-trip in url order" {
|
||||
var database = try openMigrated();
|
||||
defer database.close();
|
||||
try seedSources(&database);
|
||||
|
||||
var items = try listBlocklistSources(&database, testing.allocator);
|
||||
defer items.deinit(testing.allocator);
|
||||
defer freeBlocklistSources(testing.allocator, items.items);
|
||||
|
||||
try testing.expectEqual(@as(usize, 3), items.items.len);
|
||||
try testing.expectEqualStrings("https://a.example/list.txt", items.items[0].url);
|
||||
try testing.expectEqualStrings("A list", items.items[0].name);
|
||||
try testing.expect(!items.items[0].enabled);
|
||||
try testing.expect(!items.items[0].is_suggested);
|
||||
try testing.expectEqualStrings("https://b.example/list.txt", items.items[1].url);
|
||||
try testing.expectEqualStrings("B list", items.items[1].name);
|
||||
try testing.expect(items.items[1].enabled);
|
||||
try testing.expect(items.items[1].is_suggested);
|
||||
try testing.expectEqualStrings("https://c.example/list.txt", items.items[2].url);
|
||||
try testing.expectEqualStrings("C list", items.items[2].name);
|
||||
try testing.expect(items.items[2].enabled);
|
||||
try testing.expect(!items.items[2].is_suggested);
|
||||
}
|
||||
|
||||
test "insertBlocklistSource leaves the runtime columns at their defaults" {
|
||||
var database = try openMigrated();
|
||||
defer database.close();
|
||||
try seedSources(&database);
|
||||
|
||||
try testing.expectEqual(
|
||||
@as(i64, 3),
|
||||
try database.queryInt("SELECT count(*) FROM blocklist_sources WHERE last_updated IS NULL"),
|
||||
);
|
||||
try testing.expectEqual(
|
||||
@as(i64, 3),
|
||||
try database.queryInt("SELECT count(*) FROM blocklist_sources WHERE checksum IS NULL"),
|
||||
);
|
||||
try testing.expectEqual(
|
||||
@as(i64, 0),
|
||||
try database.queryInt("SELECT sum(domain_count + wildcard_count + skipped_regex_count) FROM blocklist_sources"),
|
||||
);
|
||||
}
|
||||
|
||||
test "deleteAllBlocklistSources empties the table and countBlocklistSources reflects it" {
|
||||
var database = try openMigrated();
|
||||
defer database.close();
|
||||
try seedSources(&database);
|
||||
|
||||
try testing.expectEqual(@as(i64, 3), try countBlocklistSources(&database));
|
||||
try deleteAllBlocklistSources(&database);
|
||||
try testing.expectEqual(@as(i64, 0), try countBlocklistSources(&database));
|
||||
}
|
||||
|
||||
fn listBlocklistSourcesUnderFailure(gpa: Allocator) !void {
|
||||
var database = try openMigrated();
|
||||
defer database.close();
|
||||
try seedSources(&database);
|
||||
|
||||
var items = try listBlocklistSources(&database, gpa);
|
||||
defer items.deinit(gpa);
|
||||
defer freeBlocklistSources(gpa, items.items);
|
||||
}
|
||||
|
||||
test "listBlocklistSources is leak-safe under allocation failure" {
|
||||
try testing.checkAllAllocationFailures(testing.allocator, listBlocklistSourcesUnderFailure, .{});
|
||||
}
|
||||
Reference in New Issue
Block a user