storage and config: sqlite wrapper, migrations, querylog policy, repositories, zon config with import/export/check cli
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
//! `local_records` and `forward_zones`.
|
||||
//!
|
||||
//! 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;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// local_records
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const list_local_records_sql =
|
||||
\\SELECT name, rtype, value, ttl FROM local_records ORDER BY name, rtype, value
|
||||
;
|
||||
|
||||
/// Every string in the result is a heap copy owned by `gpa`.
|
||||
pub fn listLocalRecords(database: *db.Db, gpa: Allocator) db.Error!std.ArrayList(model.LocalRecord) {
|
||||
var stmt = try database.prepare(list_local_records_sql);
|
||||
defer stmt.deinit();
|
||||
|
||||
var out: std.ArrayList(model.LocalRecord) = .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 freeLocalRecords(gpa, out.items);
|
||||
|
||||
while (try stmt.step()) {
|
||||
const name = try stmt.columnTextAlloc(gpa, 0);
|
||||
errdefer gpa.free(name);
|
||||
const value = try stmt.columnTextAlloc(gpa, 2);
|
||||
errdefer gpa.free(value);
|
||||
// The DDL's CHECK constraint makes the decode total for any row nxdns
|
||||
// wrote; `error.Mismatch` covers a row that something else wrote.
|
||||
const rtype = model.RecordType.fromDb(stmt.columnText(1)) orelse return error.Mismatch;
|
||||
const ttl = std.math.cast(u32, stmt.columnInt(3)) orelse return error.Mismatch;
|
||||
try out.append(gpa, .{ .name = name, .rtype = rtype, .value = value, .ttl = ttl });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
pub fn freeLocalRecords(gpa: Allocator, items: []const model.LocalRecord) void {
|
||||
for (items) |item| {
|
||||
gpa.free(item.name);
|
||||
gpa.free(item.value);
|
||||
}
|
||||
}
|
||||
|
||||
const insert_local_record_sql =
|
||||
\\INSERT INTO local_records (name, rtype, value, ttl) VALUES (?1, ?2, ?3, ?4)
|
||||
;
|
||||
|
||||
pub fn insertLocalRecord(database: *db.Db, item: model.LocalRecord, ctx: InsertContext) db.Error!void {
|
||||
_ = ctx;
|
||||
var stmt = try database.prepare(insert_local_record_sql);
|
||||
defer stmt.deinit();
|
||||
try stmt.bindText(1, item.name);
|
||||
try stmt.bindText(2, item.rtype.toDb());
|
||||
try stmt.bindText(3, item.value);
|
||||
try stmt.bindInt(4, item.ttl);
|
||||
try stmt.exec();
|
||||
}
|
||||
|
||||
pub fn deleteAllLocalRecords(database: *db.Db) db.Error!void {
|
||||
return database.exec("DELETE FROM local_records;");
|
||||
}
|
||||
|
||||
pub fn countLocalRecords(database: *db.Db) db.Error!i64 {
|
||||
return database.queryInt("SELECT count(*) FROM local_records");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// forward_zones
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn listForwardZones(database: *db.Db, gpa: Allocator) db.Error!std.ArrayList(model.ForwardZone) {
|
||||
var stmt = try database.prepare("SELECT zone, resolver FROM forward_zones ORDER BY zone");
|
||||
defer stmt.deinit();
|
||||
|
||||
var out: std.ArrayList(model.ForwardZone) = .empty;
|
||||
errdefer out.deinit(gpa);
|
||||
errdefer freeForwardZones(gpa, out.items);
|
||||
|
||||
while (try stmt.step()) {
|
||||
const zone = try stmt.columnTextAlloc(gpa, 0);
|
||||
errdefer gpa.free(zone);
|
||||
const resolver = try stmt.columnTextAlloc(gpa, 1);
|
||||
errdefer gpa.free(resolver);
|
||||
try out.append(gpa, .{ .zone = zone, .resolver = resolver });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
pub fn freeForwardZones(gpa: Allocator, items: []const model.ForwardZone) void {
|
||||
for (items) |item| {
|
||||
gpa.free(item.zone);
|
||||
gpa.free(item.resolver);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insertForwardZone(database: *db.Db, item: model.ForwardZone, ctx: InsertContext) db.Error!void {
|
||||
_ = ctx;
|
||||
var stmt = try database.prepare("INSERT INTO forward_zones (zone, resolver) VALUES (?1, ?2)");
|
||||
defer stmt.deinit();
|
||||
try stmt.bindText(1, item.zone);
|
||||
try stmt.bindText(2, item.resolver);
|
||||
try stmt.exec();
|
||||
}
|
||||
|
||||
pub fn deleteAllForwardZones(database: *db.Db) db.Error!void {
|
||||
return database.exec("DELETE FROM forward_zones;");
|
||||
}
|
||||
|
||||
pub fn countForwardZones(database: *db.Db) db.Error!i64 {
|
||||
return database.queryInt("SELECT count(*) FROM forward_zones");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 seedLocalRecords(database: *db.Db) !void {
|
||||
const ctx: InsertContext = .{};
|
||||
try insertLocalRecord(database, .{
|
||||
.name = "nas.home.arpa",
|
||||
.rtype = .aaaa,
|
||||
.value = "fd00::5",
|
||||
.ttl = 60,
|
||||
}, ctx);
|
||||
try insertLocalRecord(database, .{
|
||||
.name = "nas.home.arpa",
|
||||
.rtype = .a,
|
||||
.value = "192.168.1.5",
|
||||
}, ctx);
|
||||
try insertLocalRecord(database, .{
|
||||
.name = "alias.home.arpa",
|
||||
.rtype = .cname,
|
||||
.value = "nas.home.arpa",
|
||||
.ttl = 120,
|
||||
}, ctx);
|
||||
}
|
||||
|
||||
fn seedForwardZones(database: *db.Db) !void {
|
||||
const ctx: InsertContext = .{};
|
||||
try insertForwardZone(database, .{ .zone = "work.example", .resolver = "udp://10.0.0.1:53" }, ctx);
|
||||
try insertForwardZone(database, .{ .zone = "home.arpa", .resolver = "udp://192.168.1.1:53" }, ctx);
|
||||
try insertForwardZone(database, .{ .zone = "lab.example", .resolver = "tcp://[fd00::1]:53" }, ctx);
|
||||
}
|
||||
|
||||
test "local_records round-trip in name, rtype, value order" {
|
||||
var database = try openMigrated();
|
||||
defer database.close();
|
||||
try seedLocalRecords(&database);
|
||||
|
||||
var items = try listLocalRecords(&database, testing.allocator);
|
||||
defer items.deinit(testing.allocator);
|
||||
defer freeLocalRecords(testing.allocator, items.items);
|
||||
|
||||
// `rtype` is compared as stored text, so 'A' sorts before 'AAAA'.
|
||||
try testing.expectEqual(@as(usize, 3), items.items.len);
|
||||
try testing.expectEqualStrings("alias.home.arpa", items.items[0].name);
|
||||
try testing.expectEqual(model.RecordType.cname, items.items[0].rtype);
|
||||
try testing.expectEqualStrings("nas.home.arpa", items.items[0].value);
|
||||
try testing.expectEqual(@as(u32, 120), items.items[0].ttl);
|
||||
try testing.expectEqualStrings("nas.home.arpa", items.items[1].name);
|
||||
try testing.expectEqual(model.RecordType.a, items.items[1].rtype);
|
||||
try testing.expectEqualStrings("192.168.1.5", items.items[1].value);
|
||||
try testing.expectEqual(@as(u32, 300), items.items[1].ttl);
|
||||
try testing.expectEqualStrings("nas.home.arpa", items.items[2].name);
|
||||
try testing.expectEqual(model.RecordType.aaaa, items.items[2].rtype);
|
||||
try testing.expectEqualStrings("fd00::5", items.items[2].value);
|
||||
try testing.expectEqual(@as(u32, 60), items.items[2].ttl);
|
||||
}
|
||||
|
||||
test "deleteAllLocalRecords empties the table and countLocalRecords reflects it" {
|
||||
var database = try openMigrated();
|
||||
defer database.close();
|
||||
try seedLocalRecords(&database);
|
||||
|
||||
try testing.expectEqual(@as(i64, 3), try countLocalRecords(&database));
|
||||
try deleteAllLocalRecords(&database);
|
||||
try testing.expectEqual(@as(i64, 0), try countLocalRecords(&database));
|
||||
}
|
||||
|
||||
fn listLocalRecordsUnderFailure(gpa: Allocator) !void {
|
||||
var database = try openMigrated();
|
||||
defer database.close();
|
||||
try seedLocalRecords(&database);
|
||||
|
||||
var items = try listLocalRecords(&database, gpa);
|
||||
defer items.deinit(gpa);
|
||||
defer freeLocalRecords(gpa, items.items);
|
||||
}
|
||||
|
||||
test "listLocalRecords is leak-safe under allocation failure" {
|
||||
try testing.checkAllAllocationFailures(testing.allocator, listLocalRecordsUnderFailure, .{});
|
||||
}
|
||||
|
||||
test "forward_zones round-trip in zone order" {
|
||||
var database = try openMigrated();
|
||||
defer database.close();
|
||||
try seedForwardZones(&database);
|
||||
|
||||
var items = try listForwardZones(&database, testing.allocator);
|
||||
defer items.deinit(testing.allocator);
|
||||
defer freeForwardZones(testing.allocator, items.items);
|
||||
|
||||
try testing.expectEqual(@as(usize, 3), items.items.len);
|
||||
try testing.expectEqualStrings("home.arpa", items.items[0].zone);
|
||||
try testing.expectEqualStrings("udp://192.168.1.1:53", items.items[0].resolver);
|
||||
try testing.expectEqualStrings("lab.example", items.items[1].zone);
|
||||
try testing.expectEqualStrings("tcp://[fd00::1]:53", items.items[1].resolver);
|
||||
try testing.expectEqualStrings("work.example", items.items[2].zone);
|
||||
try testing.expectEqualStrings("udp://10.0.0.1:53", items.items[2].resolver);
|
||||
}
|
||||
|
||||
test "deleteAllForwardZones empties the table and countForwardZones reflects it" {
|
||||
var database = try openMigrated();
|
||||
defer database.close();
|
||||
try seedForwardZones(&database);
|
||||
|
||||
try testing.expectEqual(@as(i64, 3), try countForwardZones(&database));
|
||||
try deleteAllForwardZones(&database);
|
||||
try testing.expectEqual(@as(i64, 0), try countForwardZones(&database));
|
||||
}
|
||||
|
||||
fn listForwardZonesUnderFailure(gpa: Allocator) !void {
|
||||
var database = try openMigrated();
|
||||
defer database.close();
|
||||
try seedForwardZones(&database);
|
||||
|
||||
var items = try listForwardZones(&database, gpa);
|
||||
defer items.deinit(gpa);
|
||||
defer freeForwardZones(gpa, items.items);
|
||||
}
|
||||
|
||||
test "listForwardZones is leak-safe under allocation failure" {
|
||||
try testing.checkAllAllocationFailures(testing.allocator, listForwardZonesUnderFailure, .{});
|
||||
}
|
||||
Reference in New Issue
Block a user