storage and config: sqlite wrapper, migrations, querylog policy, repositories, zon config with import/export/check cli
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
//! `upstreams`.
|
||||
//!
|
||||
//! The list sorts by `priority` first because that is the operationally
|
||||
//! meaningful order — it matches what `Pool.init` expects — and `url` breaks
|
||||
//! ties uniquely, which is what makes an export byte-stable.
|
||||
//!
|
||||
//! 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;
|
||||
|
||||
/// Every string in the result is a heap copy owned by `gpa`.
|
||||
pub fn listUpstreams(database: *db.Db, gpa: Allocator) db.Error!std.ArrayList(model.UpstreamServer) {
|
||||
var stmt = try database.prepare("SELECT url, priority, enabled FROM upstreams ORDER BY priority, url");
|
||||
defer stmt.deinit();
|
||||
|
||||
var out: std.ArrayList(model.UpstreamServer) = .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 freeUpstreams(gpa, out.items);
|
||||
|
||||
while (try stmt.step()) {
|
||||
const url = try stmt.columnTextAlloc(gpa, 0);
|
||||
errdefer gpa.free(url);
|
||||
const priority = std.math.cast(i32, stmt.columnInt(1)) orelse return error.Mismatch;
|
||||
try out.append(gpa, .{ .url = url, .priority = priority, .enabled = stmt.columnBool(2) });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
pub fn freeUpstreams(gpa: Allocator, items: []const model.UpstreamServer) void {
|
||||
for (items) |item| gpa.free(item.url);
|
||||
}
|
||||
|
||||
pub fn insertUpstream(database: *db.Db, item: model.UpstreamServer, ctx: InsertContext) db.Error!void {
|
||||
_ = ctx;
|
||||
var stmt = try database.prepare("INSERT INTO upstreams (url, priority, enabled) VALUES (?1, ?2, ?3)");
|
||||
defer stmt.deinit();
|
||||
try stmt.bindText(1, item.url);
|
||||
try stmt.bindInt(2, item.priority);
|
||||
try stmt.bindBool(3, item.enabled);
|
||||
try stmt.exec();
|
||||
}
|
||||
|
||||
pub fn deleteAllUpstreams(database: *db.Db) db.Error!void {
|
||||
return database.exec("DELETE FROM upstreams;");
|
||||
}
|
||||
|
||||
pub fn countUpstreams(database: *db.Db) db.Error!i64 {
|
||||
return database.queryInt("SELECT count(*) FROM upstreams");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 seedUpstreams(database: *db.Db) !void {
|
||||
const ctx: InsertContext = .{};
|
||||
try insertUpstream(database, .{ .url = "https://dns.example/dns-query", .priority = 50 }, ctx);
|
||||
try insertUpstream(database, .{ .url = "tls://1.1.1.1:853", .priority = 10, .enabled = false }, ctx);
|
||||
try insertUpstream(database, .{ .url = "https://a.example/dns-query", .priority = 50 }, ctx);
|
||||
}
|
||||
|
||||
test "upstreams round-trip in priority then url order" {
|
||||
var database = try openMigrated();
|
||||
defer database.close();
|
||||
try seedUpstreams(&database);
|
||||
|
||||
var items = try listUpstreams(&database, testing.allocator);
|
||||
defer items.deinit(testing.allocator);
|
||||
defer freeUpstreams(testing.allocator, items.items);
|
||||
|
||||
try testing.expectEqual(@as(usize, 3), items.items.len);
|
||||
try testing.expectEqualStrings("tls://1.1.1.1:853", items.items[0].url);
|
||||
try testing.expectEqual(@as(i32, 10), items.items[0].priority);
|
||||
try testing.expect(!items.items[0].enabled);
|
||||
try testing.expectEqualStrings("https://a.example/dns-query", items.items[1].url);
|
||||
try testing.expectEqual(@as(i32, 50), items.items[1].priority);
|
||||
try testing.expect(items.items[1].enabled);
|
||||
try testing.expectEqualStrings("https://dns.example/dns-query", items.items[2].url);
|
||||
try testing.expectEqual(@as(i32, 50), items.items[2].priority);
|
||||
try testing.expect(items.items[2].enabled);
|
||||
}
|
||||
|
||||
test "deleteAllUpstreams empties the table and countUpstreams reflects it" {
|
||||
var database = try openMigrated();
|
||||
defer database.close();
|
||||
try seedUpstreams(&database);
|
||||
|
||||
try testing.expectEqual(@as(i64, 3), try countUpstreams(&database));
|
||||
try deleteAllUpstreams(&database);
|
||||
try testing.expectEqual(@as(i64, 0), try countUpstreams(&database));
|
||||
|
||||
var items = try listUpstreams(&database, testing.allocator);
|
||||
defer items.deinit(testing.allocator);
|
||||
defer freeUpstreams(testing.allocator, items.items);
|
||||
try testing.expectEqual(@as(usize, 0), items.items.len);
|
||||
}
|
||||
|
||||
fn listUpstreamsUnderFailure(gpa: Allocator) !void {
|
||||
var database = try openMigrated();
|
||||
defer database.close();
|
||||
try seedUpstreams(&database);
|
||||
|
||||
var items = try listUpstreams(&database, gpa);
|
||||
defer items.deinit(gpa);
|
||||
defer freeUpstreams(gpa, items.items);
|
||||
}
|
||||
|
||||
test "listUpstreams is leak-safe under allocation failure" {
|
||||
try testing.checkAllAllocationFailures(testing.allocator, listUpstreamsUnderFailure, .{});
|
||||
}
|
||||
Reference in New Issue
Block a user