milestone 18: collapse duplicated infrastructure into shared listener core, crud list helper, resource shells, transport race, name and line helpers, ui modules
This commit is contained in:
@@ -38,35 +38,23 @@ const list_clients_sql =
|
||||
|
||||
/// Every string in the result is a heap copy owned by `gpa`.
|
||||
pub fn listClients(database: *db.Db, gpa: Allocator) db.Error!std.ArrayList(model.Client) {
|
||||
var stmt = try database.prepare(list_clients_sql);
|
||||
defer stmt.deinit();
|
||||
return crud.listRows(model.Client, database, gpa, list_clients_sql, readClient);
|
||||
}
|
||||
|
||||
var out: std.ArrayList(model.Client) = .empty;
|
||||
// `errdefer`s run in reverse: `freeClients` is declared last so it runs
|
||||
// before the backing array is released.
|
||||
errdefer out.deinit(gpa);
|
||||
errdefer freeClients(gpa, out.items);
|
||||
|
||||
while (try stmt.step()) {
|
||||
const ip = try stmt.columnTextAlloc(gpa, 0);
|
||||
errdefer gpa.free(ip);
|
||||
// `clients.name` is nullable; `columnTextAlloc` reads NULL as "", which
|
||||
// is exactly the model's default.
|
||||
const name = try stmt.columnTextAlloc(gpa, 1);
|
||||
errdefer gpa.free(name);
|
||||
const group = try stmt.columnTextAlloc(gpa, 2);
|
||||
errdefer gpa.free(group);
|
||||
try out.append(gpa, .{ .ip = ip, .name = name, .group = group });
|
||||
}
|
||||
return out;
|
||||
fn readClient(stmt: *db.Stmt, gpa: Allocator) db.Error!model.Client {
|
||||
const ip = try stmt.columnTextAlloc(gpa, 0);
|
||||
errdefer gpa.free(ip);
|
||||
// `clients.name` is nullable; `columnTextAlloc` reads NULL as "", which is
|
||||
// exactly the model's default.
|
||||
const name = try stmt.columnTextAlloc(gpa, 1);
|
||||
errdefer gpa.free(name);
|
||||
const group = try stmt.columnTextAlloc(gpa, 2);
|
||||
errdefer gpa.free(group);
|
||||
return .{ .ip = ip, .name = name, .group = group };
|
||||
}
|
||||
|
||||
pub fn freeClients(gpa: Allocator, items: []const model.Client) void {
|
||||
for (items) |item| {
|
||||
gpa.free(item.ip);
|
||||
gpa.free(item.name);
|
||||
gpa.free(item.group);
|
||||
}
|
||||
crud.freeRows(model.Client, gpa, items);
|
||||
}
|
||||
|
||||
const insert_client_sql =
|
||||
@@ -154,31 +142,22 @@ const list_client_prefixes_sql =
|
||||
;
|
||||
|
||||
pub fn listClientPrefixes(database: *db.Db, gpa: Allocator) db.Error!std.ArrayList(model.ClientPrefix) {
|
||||
var stmt = try database.prepare(list_client_prefixes_sql);
|
||||
defer stmt.deinit();
|
||||
return crud.listRows(model.ClientPrefix, database, gpa, list_client_prefixes_sql, readClientPrefix);
|
||||
}
|
||||
|
||||
var out: std.ArrayList(model.ClientPrefix) = .empty;
|
||||
errdefer out.deinit(gpa);
|
||||
errdefer freeClientPrefixes(gpa, out.items);
|
||||
|
||||
while (try stmt.step()) {
|
||||
const prefix = try stmt.columnTextAlloc(gpa, 0);
|
||||
errdefer gpa.free(prefix);
|
||||
const group = try stmt.columnTextAlloc(gpa, 1);
|
||||
errdefer gpa.free(group);
|
||||
// The column is a 64-bit integer; the model field is `i32`. A value
|
||||
// outside that range means something other than nxdns wrote the row.
|
||||
const priority = std.math.cast(i32, stmt.columnInt(2)) orelse return error.Mismatch;
|
||||
try out.append(gpa, .{ .prefix = prefix, .group = group, .priority = priority });
|
||||
}
|
||||
return out;
|
||||
fn readClientPrefix(stmt: *db.Stmt, gpa: Allocator) db.Error!model.ClientPrefix {
|
||||
const prefix = try stmt.columnTextAlloc(gpa, 0);
|
||||
errdefer gpa.free(prefix);
|
||||
const group = try stmt.columnTextAlloc(gpa, 1);
|
||||
errdefer gpa.free(group);
|
||||
// The column is a 64-bit integer; the model field is `i32`. A value outside
|
||||
// that range means something other than nxdns wrote the row.
|
||||
const priority = std.math.cast(i32, stmt.columnInt(2)) orelse return error.Mismatch;
|
||||
return .{ .prefix = prefix, .group = group, .priority = priority };
|
||||
}
|
||||
|
||||
pub fn freeClientPrefixes(gpa: Allocator, items: []const model.ClientPrefix) void {
|
||||
for (items) |item| {
|
||||
gpa.free(item.prefix);
|
||||
gpa.free(item.group);
|
||||
}
|
||||
crud.freeRows(model.ClientPrefix, gpa, items);
|
||||
}
|
||||
|
||||
pub fn insertClientPrefix(database: *db.Db, item: model.ClientPrefix, ctx: InsertContext) db.Error!void {
|
||||
@@ -257,29 +236,15 @@ const get_client_sql =
|
||||
/// Every client, materialised ones included. Every string is a heap copy owned
|
||||
/// by `gpa`.
|
||||
pub fn listClientRows(database: *db.Db, gpa: Allocator) db.Error!std.ArrayList(ClientRow) {
|
||||
var stmt = try database.prepare(list_client_rows_sql);
|
||||
defer stmt.deinit();
|
||||
|
||||
var out: std.ArrayList(ClientRow) = .empty;
|
||||
errdefer out.deinit(gpa);
|
||||
errdefer freeClientRows(gpa, out.items);
|
||||
|
||||
while (try stmt.step()) {
|
||||
const row = try readClientRow(&stmt, gpa);
|
||||
errdefer freeClientRow(gpa, row);
|
||||
try out.append(gpa, row);
|
||||
}
|
||||
return out;
|
||||
return crud.listRows(ClientRow, database, gpa, list_client_rows_sql, readClientRow);
|
||||
}
|
||||
|
||||
pub fn freeClientRow(gpa: Allocator, row: ClientRow) void {
|
||||
gpa.free(row.ip);
|
||||
gpa.free(row.name);
|
||||
gpa.free(row.group);
|
||||
crud.freeRow(ClientRow, gpa, row);
|
||||
}
|
||||
|
||||
pub fn freeClientRows(gpa: Allocator, items: []const ClientRow) void {
|
||||
for (items) |item| freeClientRow(gpa, item);
|
||||
crud.freeRows(ClientRow, gpa, items);
|
||||
}
|
||||
|
||||
pub fn getClient(database: *db.Db, gpa: Allocator, id: i64) db.Error!?ClientRow {
|
||||
@@ -383,39 +348,32 @@ const list_client_prefix_rows_sql =
|
||||
/// Same order as `listClientPrefixes`; every string is a heap copy owned by
|
||||
/// `gpa`.
|
||||
pub fn listClientPrefixRows(database: *db.Db, gpa: Allocator) db.Error!std.ArrayList(ClientPrefixRow) {
|
||||
var stmt = try database.prepare(list_client_prefix_rows_sql);
|
||||
defer stmt.deinit();
|
||||
return crud.listRows(ClientPrefixRow, database, gpa, list_client_prefix_rows_sql, readClientPrefixRow);
|
||||
}
|
||||
|
||||
var out: std.ArrayList(ClientPrefixRow) = .empty;
|
||||
errdefer out.deinit(gpa);
|
||||
errdefer freeClientPrefixRows(gpa, out.items);
|
||||
|
||||
while (try stmt.step()) {
|
||||
const prefix = try stmt.columnTextAlloc(gpa, 1);
|
||||
errdefer gpa.free(prefix);
|
||||
const group = try stmt.columnTextAlloc(gpa, 3);
|
||||
errdefer gpa.free(group);
|
||||
// The column is a 64-bit integer; the row field is `i32`. A value
|
||||
// outside that range means something other than nxdns wrote the row.
|
||||
const priority = std.math.cast(i32, stmt.columnInt(4)) orelse return error.Mismatch;
|
||||
try out.append(gpa, .{
|
||||
.id = stmt.columnInt(0),
|
||||
.prefix = prefix,
|
||||
.group_id = stmt.columnInt(2),
|
||||
.group = group,
|
||||
.priority = priority,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
fn readClientPrefixRow(stmt: *db.Stmt, gpa: Allocator) db.Error!ClientPrefixRow {
|
||||
const prefix = try stmt.columnTextAlloc(gpa, 1);
|
||||
errdefer gpa.free(prefix);
|
||||
const group = try stmt.columnTextAlloc(gpa, 3);
|
||||
errdefer gpa.free(group);
|
||||
// The column is a 64-bit integer; the row field is `i32`. A value outside
|
||||
// that range means something other than nxdns wrote the row.
|
||||
const priority = std.math.cast(i32, stmt.columnInt(4)) orelse return error.Mismatch;
|
||||
return .{
|
||||
.id = stmt.columnInt(0),
|
||||
.prefix = prefix,
|
||||
.group_id = stmt.columnInt(2),
|
||||
.group = group,
|
||||
.priority = priority,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn freeClientPrefixRow(gpa: Allocator, row: ClientPrefixRow) void {
|
||||
gpa.free(row.prefix);
|
||||
gpa.free(row.group);
|
||||
crud.freeRow(ClientPrefixRow, gpa, row);
|
||||
}
|
||||
|
||||
pub fn freeClientPrefixRows(gpa: Allocator, items: []const ClientPrefixRow) void {
|
||||
for (items) |item| freeClientPrefixRow(gpa, item);
|
||||
crud.freeRows(ClientPrefixRow, gpa, items);
|
||||
}
|
||||
|
||||
/// Replaces the whole prefix table inside a transaction (ruling 9 makes
|
||||
|
||||
@@ -8,8 +8,14 @@
|
||||
//! `error.Constraint` needs no helper — `Stmt.exec` already reports it, and the
|
||||
//! handler layer maps it to 409. Each mutation documents which constraint of
|
||||
//! `config_schema.ddl_v1` can fire.
|
||||
//!
|
||||
//! `listRows` and `freeRows` are the read half. Every `list*` function in this
|
||||
//! directory reads rows into a `std.ArrayList` under the same unwind rules, and
|
||||
//! the errdefer ordering those rules need is easy to write backwards. It is
|
||||
//! written once here.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const db = @import("../db.zig");
|
||||
const migrations = @import("../migrations.zig");
|
||||
@@ -25,6 +31,102 @@ pub fn execStrict(database: *db.Db, stmt: *db.Stmt) db.Error!void {
|
||||
if (database.changes() == 0) return error.NotFound;
|
||||
}
|
||||
|
||||
/// Reads every row `sql` produces into a list, with the memory-safety
|
||||
/// choreography every `list*` function in this directory shares.
|
||||
///
|
||||
/// `readRow` allocates the row's owning fields from `gpa` and carries its own
|
||||
/// per-column `errdefer`s, so a row that fails halfway releases the columns it
|
||||
/// already read. This function owns everything around that: a failure after the
|
||||
/// first append releases the rows already in the list and then the list itself.
|
||||
///
|
||||
/// The result is the caller's: free the rows with `freeRows` (or the repository
|
||||
/// shim over it) and then `deinit` the list.
|
||||
pub fn listRows(
|
||||
comptime Row: type,
|
||||
database: *db.Db,
|
||||
gpa: Allocator,
|
||||
comptime sql: []const u8,
|
||||
comptime readRow: fn (*db.Stmt, Allocator) db.Error!Row,
|
||||
) db.Error!std.ArrayList(Row) {
|
||||
return listRowsBound(Row, database, gpa, sql, readRow, .{});
|
||||
}
|
||||
|
||||
/// `listRows` for a statement with parameters. `args` is a tuple bound to
|
||||
/// positions 1..n in order.
|
||||
pub fn listRowsBound(
|
||||
comptime Row: type,
|
||||
database: *db.Db,
|
||||
gpa: Allocator,
|
||||
comptime sql: []const u8,
|
||||
comptime readRow: fn (*db.Stmt, Allocator) db.Error!Row,
|
||||
args: anytype,
|
||||
) db.Error!std.ArrayList(Row) {
|
||||
var stmt = try database.prepare(sql);
|
||||
defer stmt.deinit();
|
||||
inline for (args, 0..) |arg, i| try bindArg(&stmt, i + 1, arg);
|
||||
|
||||
var out: std.ArrayList(Row) = .empty;
|
||||
// Order matters: `errdefer`s run in reverse, so the free pass is declared
|
||||
// *after* `deinit` to run *before* it. The other order reads `out.items`
|
||||
// after the backing array is gone.
|
||||
errdefer out.deinit(gpa);
|
||||
errdefer freeRows(Row, gpa, out.items);
|
||||
|
||||
while (try stmt.step()) {
|
||||
const row = try readRow(&stmt, gpa);
|
||||
errdefer freeRow(Row, gpa, row);
|
||||
try out.append(gpa, row);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
fn bindArg(stmt: *db.Stmt, idx: c_int, arg: anytype) db.Error!void {
|
||||
const Arg = @TypeOf(arg);
|
||||
if (Arg == []const u8 or Arg == []u8) return stmt.bindText(idx, arg);
|
||||
return switch (@typeInfo(Arg)) {
|
||||
.bool => stmt.bindInt(idx, @intFromBool(arg)),
|
||||
.int, .comptime_int => stmt.bindInt(idx, arg),
|
||||
else => @compileError("crud.listRowsBound: cannot bind a " ++ @typeName(Arg)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Releases every owning field of every row `listRows` produced.
|
||||
pub fn freeRows(comptime Row: type, gpa: Allocator, items: []const Row) void {
|
||||
for (items) |item| freeRow(Row, gpa, item);
|
||||
}
|
||||
|
||||
/// Releases the owning fields of one row.
|
||||
///
|
||||
/// A repository row owns its heap memory in exactly two shapes: `[]const u8`
|
||||
/// and `?[]const u8` (`SourceRow.checksum` is the optional one — a shallow
|
||||
/// slice-only reflection would leak its payload). Every other field must be a
|
||||
/// plain value the row does not own. A field of any other shape is a
|
||||
/// `@compileError`, so a row that grows a nested allocation cannot start
|
||||
/// leaking silently: whoever adds it has to teach this function first.
|
||||
pub fn freeRow(comptime Row: type, gpa: Allocator, row: Row) void {
|
||||
switch (@typeInfo(Row)) {
|
||||
.@"struct" => |info| inline for (info.fields) |field| {
|
||||
freeField(field.type, @typeName(Row) ++ "." ++ field.name, gpa, @field(row, field.name));
|
||||
},
|
||||
else => freeField(Row, @typeName(Row), gpa, row),
|
||||
}
|
||||
}
|
||||
|
||||
fn freeField(comptime Field: type, comptime where: []const u8, gpa: Allocator, value: Field) void {
|
||||
if (Field == []const u8 or Field == []u8) return gpa.free(value);
|
||||
if (Field == ?[]const u8 or Field == ?[]u8) return if (value) |owned| gpa.free(owned);
|
||||
comptime assertUnowning(Field, where);
|
||||
}
|
||||
|
||||
fn assertUnowning(comptime Field: type, comptime where: []const u8) void {
|
||||
switch (@typeInfo(Field)) {
|
||||
.bool, .int, .float, .@"enum", .void => {},
|
||||
.optional => |info| assertUnowning(info.child, where),
|
||||
else => @compileError("crud.freeRow: " ++ where ++ " is a " ++ @typeName(Field) ++
|
||||
", which is neither a plain value nor an owning slice; teach freeRow how to release it"),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// tests
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -78,3 +180,91 @@ test "execStrict reports NotFound for an id no row holds" {
|
||||
|
||||
try testing.expectEqual(@as(i64, 1), try database.queryInt("SELECT count(*) FROM forward_zones"));
|
||||
}
|
||||
|
||||
/// Carries both owning shapes a repository row may hold: a `[]const u8` that is
|
||||
/// always there, and the `?[]const u8` of `SourceRow.checksum`.
|
||||
const TestRow = struct {
|
||||
id: i64,
|
||||
url: []const u8,
|
||||
checksum: ?[]const u8,
|
||||
};
|
||||
|
||||
const test_rows_sql = "SELECT id, url, checksum FROM blocklist_sources ORDER BY id";
|
||||
|
||||
fn readTestRow(stmt: *db.Stmt, gpa: Allocator) db.Error!TestRow {
|
||||
const url = try stmt.columnTextAlloc(gpa, 1);
|
||||
errdefer gpa.free(url);
|
||||
const checksum = try stmt.columnTextAllocOrNull(gpa, 2);
|
||||
errdefer if (checksum) |value| gpa.free(value);
|
||||
return .{ .id = stmt.columnInt(0), .url = url, .checksum = checksum };
|
||||
}
|
||||
|
||||
/// Two rows: the first carries a checksum, the second leaves it NULL, so one
|
||||
/// read exercises both arms of the optional.
|
||||
fn seedTestRows(database: *db.Db) !void {
|
||||
try database.exec(
|
||||
\\INSERT INTO blocklist_sources (id, url, name, checksum) VALUES
|
||||
\\ (1, 'https://a.example/list.txt', 'A', 'aaaa'),
|
||||
\\ (2, 'https://b.example/list.txt', 'B', NULL);
|
||||
);
|
||||
}
|
||||
|
||||
test "listRows reads every row and freeRows releases both owning shapes" {
|
||||
var database = try openTable();
|
||||
defer database.close();
|
||||
try seedTestRows(&database);
|
||||
|
||||
var rows = try listRows(TestRow, &database, testing.allocator, test_rows_sql, readTestRow);
|
||||
defer rows.deinit(testing.allocator);
|
||||
defer freeRows(TestRow, testing.allocator, rows.items);
|
||||
|
||||
try testing.expectEqual(@as(usize, 2), rows.items.len);
|
||||
try testing.expectEqualStrings("https://a.example/list.txt", rows.items[0].url);
|
||||
// The leak detector is what proves this payload is released.
|
||||
try testing.expectEqualStrings("aaaa", rows.items[0].checksum.?);
|
||||
try testing.expectEqual(@as(?[]const u8, null), rows.items[1].checksum);
|
||||
}
|
||||
|
||||
fn listRowsUnderFailure(gpa: Allocator) !void {
|
||||
var database = try openTable();
|
||||
defer database.close();
|
||||
try seedTestRows(&database);
|
||||
|
||||
var rows = try listRows(TestRow, &database, gpa, test_rows_sql, readTestRow);
|
||||
defer rows.deinit(gpa);
|
||||
defer freeRows(TestRow, gpa, rows.items);
|
||||
|
||||
// A row with a non-null checksum must be in the result, or the failure
|
||||
// injection never reaches the optional's allocation.
|
||||
try testing.expect(rows.items[0].checksum != null);
|
||||
}
|
||||
|
||||
test "listRows is leak-safe under allocation failure" {
|
||||
try testing.checkAllAllocationFailures(testing.allocator, listRowsUnderFailure, .{});
|
||||
}
|
||||
|
||||
test "listRowsBound binds its arguments in tuple order" {
|
||||
var database = try openTable();
|
||||
defer database.close();
|
||||
try seedTestRows(&database);
|
||||
|
||||
var rows = try listRowsBound(
|
||||
TestRow,
|
||||
&database,
|
||||
testing.allocator,
|
||||
"SELECT id, url, checksum FROM blocklist_sources WHERE id > ?1 AND name = ?2",
|
||||
readTestRow,
|
||||
.{ @as(i64, 1), @as([]const u8, "B") },
|
||||
);
|
||||
defer rows.deinit(testing.allocator);
|
||||
defer freeRows(TestRow, testing.allocator, rows.items);
|
||||
|
||||
try testing.expectEqual(@as(usize, 1), rows.items.len);
|
||||
try testing.expectEqual(@as(i64, 2), rows.items[0].id);
|
||||
}
|
||||
|
||||
test "freeRows over a row type with no owning field is a no-op" {
|
||||
// `listGroupSourceIds` reads a bare `i64`; the reflection must accept a Row
|
||||
// that is not a struct at all.
|
||||
freeRows(i64, testing.allocator, &.{ 1, 2, 3 });
|
||||
}
|
||||
|
||||
@@ -27,26 +27,23 @@ const InsertContext = context.InsertContext;
|
||||
/// Every string in the result is a heap copy owned by `gpa`; free the whole
|
||||
/// list with `freeGroups` and then `deinit` the list itself.
|
||||
pub fn listGroups(database: *db.Db, gpa: Allocator) db.Error!std.ArrayList(model.Group) {
|
||||
var stmt = try database.prepare("SELECT name, safe_search FROM groups ORDER BY name");
|
||||
defer stmt.deinit();
|
||||
return crud.listRows(
|
||||
model.Group,
|
||||
database,
|
||||
gpa,
|
||||
"SELECT name, safe_search FROM groups ORDER BY name",
|
||||
readGroup,
|
||||
);
|
||||
}
|
||||
|
||||
var out: std.ArrayList(model.Group) = .empty;
|
||||
// Order matters: `errdefer`s run in reverse, so `freeGroups` must be
|
||||
// declared *after* `deinit` to run *before* it. The other order reads
|
||||
// `out.items` after the backing array is gone.
|
||||
errdefer out.deinit(gpa);
|
||||
errdefer freeGroups(gpa, out.items);
|
||||
|
||||
while (try stmt.step()) {
|
||||
const name = try stmt.columnTextAlloc(gpa, 0);
|
||||
errdefer gpa.free(name);
|
||||
try out.append(gpa, .{ .name = name, .safe_search = stmt.columnBool(1) });
|
||||
}
|
||||
return out;
|
||||
fn readGroup(stmt: *db.Stmt, gpa: Allocator) db.Error!model.Group {
|
||||
const name = try stmt.columnTextAlloc(gpa, 0);
|
||||
errdefer gpa.free(name);
|
||||
return .{ .name = name, .safe_search = stmt.columnBool(1) };
|
||||
}
|
||||
|
||||
pub fn freeGroups(gpa: Allocator, items: []const model.Group) void {
|
||||
for (items) |item| gpa.free(item.name);
|
||||
crud.freeRows(model.Group, gpa, items);
|
||||
}
|
||||
|
||||
pub fn insertGroup(database: *db.Db, item: model.Group, ctx: InsertContext) db.Error!void {
|
||||
@@ -91,28 +88,19 @@ const list_group_sources_sql =
|
||||
/// The two foreign keys are `NOT NULL` and enforced, so the join is total: a
|
||||
/// `group_sources` row can never be dropped by it.
|
||||
pub fn listGroupSources(database: *db.Db, gpa: Allocator) db.Error!std.ArrayList(model.GroupSource) {
|
||||
var stmt = try database.prepare(list_group_sources_sql);
|
||||
defer stmt.deinit();
|
||||
return crud.listRows(model.GroupSource, database, gpa, list_group_sources_sql, readGroupSource);
|
||||
}
|
||||
|
||||
var out: std.ArrayList(model.GroupSource) = .empty;
|
||||
errdefer out.deinit(gpa);
|
||||
errdefer freeGroupSources(gpa, out.items);
|
||||
|
||||
while (try stmt.step()) {
|
||||
const group = try stmt.columnTextAlloc(gpa, 0);
|
||||
errdefer gpa.free(group);
|
||||
const source_url = try stmt.columnTextAlloc(gpa, 1);
|
||||
errdefer gpa.free(source_url);
|
||||
try out.append(gpa, .{ .group = group, .source_url = source_url });
|
||||
}
|
||||
return out;
|
||||
fn readGroupSource(stmt: *db.Stmt, gpa: Allocator) db.Error!model.GroupSource {
|
||||
const group = try stmt.columnTextAlloc(gpa, 0);
|
||||
errdefer gpa.free(group);
|
||||
const source_url = try stmt.columnTextAlloc(gpa, 1);
|
||||
errdefer gpa.free(source_url);
|
||||
return .{ .group = group, .source_url = source_url };
|
||||
}
|
||||
|
||||
pub fn freeGroupSources(gpa: Allocator, items: []const model.GroupSource) void {
|
||||
for (items) |item| {
|
||||
gpa.free(item.group);
|
||||
gpa.free(item.source_url);
|
||||
}
|
||||
crud.freeRows(model.GroupSource, gpa, items);
|
||||
}
|
||||
|
||||
pub fn insertGroupSource(database: *db.Db, item: model.GroupSource, ctx: InsertContext) db.Error!void {
|
||||
@@ -145,31 +133,27 @@ pub const GroupRow = struct { id: i64, name: []const u8, safe_search: bool };
|
||||
|
||||
/// Same order as `listGroups`; every string is a heap copy owned by `gpa`.
|
||||
pub fn listGroupRows(database: *db.Db, gpa: Allocator) db.Error!std.ArrayList(GroupRow) {
|
||||
var stmt = try database.prepare("SELECT id, name, safe_search FROM groups ORDER BY name");
|
||||
defer stmt.deinit();
|
||||
return crud.listRows(
|
||||
GroupRow,
|
||||
database,
|
||||
gpa,
|
||||
"SELECT id, name, safe_search FROM groups ORDER BY name",
|
||||
readGroupRow,
|
||||
);
|
||||
}
|
||||
|
||||
var out: std.ArrayList(GroupRow) = .empty;
|
||||
errdefer out.deinit(gpa);
|
||||
errdefer freeGroupRows(gpa, out.items);
|
||||
|
||||
while (try stmt.step()) {
|
||||
const name = try stmt.columnTextAlloc(gpa, 1);
|
||||
errdefer gpa.free(name);
|
||||
try out.append(gpa, .{
|
||||
.id = stmt.columnInt(0),
|
||||
.name = name,
|
||||
.safe_search = stmt.columnBool(2),
|
||||
});
|
||||
}
|
||||
return out;
|
||||
fn readGroupRow(stmt: *db.Stmt, gpa: Allocator) db.Error!GroupRow {
|
||||
const name = try stmt.columnTextAlloc(gpa, 1);
|
||||
errdefer gpa.free(name);
|
||||
return .{ .id = stmt.columnInt(0), .name = name, .safe_search = stmt.columnBool(2) };
|
||||
}
|
||||
|
||||
pub fn freeGroupRow(gpa: Allocator, row: GroupRow) void {
|
||||
gpa.free(row.name);
|
||||
crud.freeRow(GroupRow, gpa, row);
|
||||
}
|
||||
|
||||
pub fn freeGroupRows(gpa: Allocator, items: []const GroupRow) void {
|
||||
for (items) |item| freeGroupRow(gpa, item);
|
||||
crud.freeRows(GroupRow, gpa, items);
|
||||
}
|
||||
|
||||
pub fn getGroup(database: *db.Db, gpa: Allocator, id: i64) db.Error!?GroupRow {
|
||||
@@ -177,11 +161,7 @@ pub fn getGroup(database: *db.Db, gpa: Allocator, id: i64) db.Error!?GroupRow {
|
||||
defer stmt.deinit();
|
||||
try stmt.bindInt(1, id);
|
||||
if (!try stmt.step()) return null;
|
||||
return .{
|
||||
.id = stmt.columnInt(0),
|
||||
.name = try stmt.columnTextAlloc(gpa, 1),
|
||||
.safe_search = stmt.columnBool(2),
|
||||
};
|
||||
return try readGroupRow(&stmt, gpa);
|
||||
}
|
||||
|
||||
/// `error.Constraint`: `groups.name` is UNIQUE.
|
||||
@@ -220,14 +200,19 @@ pub fn deleteGroup(database: *db.Db, id: i64) db.Error!void {
|
||||
/// `group_id` yields an empty list, not an error: the caller that needs the
|
||||
/// distinction reads the group itself.
|
||||
pub fn listGroupSourceIds(database: *db.Db, gpa: Allocator, group_id: i64) db.Error!std.ArrayList(i64) {
|
||||
var stmt = try database.prepare("SELECT source_id FROM group_sources WHERE group_id = ?1 ORDER BY source_id");
|
||||
defer stmt.deinit();
|
||||
try stmt.bindInt(1, group_id);
|
||||
return crud.listRowsBound(
|
||||
i64,
|
||||
database,
|
||||
gpa,
|
||||
"SELECT source_id FROM group_sources WHERE group_id = ?1 ORDER BY source_id",
|
||||
readSourceId,
|
||||
.{group_id},
|
||||
);
|
||||
}
|
||||
|
||||
var out: std.ArrayList(i64) = .empty;
|
||||
errdefer out.deinit(gpa);
|
||||
while (try stmt.step()) try out.append(gpa, stmt.columnInt(0));
|
||||
return out;
|
||||
fn readSourceId(stmt: *db.Stmt, gpa: Allocator) db.Error!i64 {
|
||||
_ = gpa;
|
||||
return stmt.columnInt(0);
|
||||
}
|
||||
|
||||
/// Replaces one group's whole source assignment inside a transaction, so a
|
||||
|
||||
@@ -25,34 +25,23 @@ const list_local_records_sql =
|
||||
|
||||
/// 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();
|
||||
return crud.listRows(model.LocalRecord, database, gpa, list_local_records_sql, readLocalRecord);
|
||||
}
|
||||
|
||||
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;
|
||||
fn readLocalRecord(stmt: *db.Stmt, gpa: Allocator) db.Error!model.LocalRecord {
|
||||
// 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;
|
||||
const name = try stmt.columnTextAlloc(gpa, 0);
|
||||
errdefer gpa.free(name);
|
||||
const value = try stmt.columnTextAlloc(gpa, 2);
|
||||
errdefer gpa.free(value);
|
||||
return .{ .name = name, .rtype = rtype, .value = value, .ttl = ttl };
|
||||
}
|
||||
|
||||
pub fn freeLocalRecords(gpa: Allocator, items: []const model.LocalRecord) void {
|
||||
for (items) |item| {
|
||||
gpa.free(item.name);
|
||||
gpa.free(item.value);
|
||||
}
|
||||
crud.freeRows(model.LocalRecord, gpa, items);
|
||||
}
|
||||
|
||||
const insert_local_record_sql =
|
||||
@@ -83,28 +72,25 @@ pub fn countLocalRecords(database: *db.Db) db.Error!i64 {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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();
|
||||
return crud.listRows(
|
||||
model.ForwardZone,
|
||||
database,
|
||||
gpa,
|
||||
"SELECT zone, resolver FROM forward_zones ORDER BY zone",
|
||||
readForwardZone,
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
fn readForwardZone(stmt: *db.Stmt, gpa: Allocator) db.Error!model.ForwardZone {
|
||||
const zone = try stmt.columnTextAlloc(gpa, 0);
|
||||
errdefer gpa.free(zone);
|
||||
const resolver = try stmt.columnTextAlloc(gpa, 1);
|
||||
errdefer gpa.free(resolver);
|
||||
return .{ .zone = zone, .resolver = resolver };
|
||||
}
|
||||
|
||||
pub fn freeForwardZones(gpa: Allocator, items: []const model.ForwardZone) void {
|
||||
for (items) |item| {
|
||||
gpa.free(item.zone);
|
||||
gpa.free(item.resolver);
|
||||
}
|
||||
crud.freeRows(model.ForwardZone, gpa, items);
|
||||
}
|
||||
|
||||
pub fn insertForwardZone(database: *db.Db, item: model.ForwardZone, ctx: InsertContext) db.Error!void {
|
||||
@@ -145,28 +131,15 @@ const list_local_record_rows_sql =
|
||||
|
||||
/// Same order as `listLocalRecords`; every string is a heap copy owned by `gpa`.
|
||||
pub fn listLocalRecordRows(database: *db.Db, gpa: Allocator) db.Error!std.ArrayList(LocalRecordRow) {
|
||||
var stmt = try database.prepare(list_local_record_rows_sql);
|
||||
defer stmt.deinit();
|
||||
|
||||
var out: std.ArrayList(LocalRecordRow) = .empty;
|
||||
errdefer out.deinit(gpa);
|
||||
errdefer freeLocalRecordRows(gpa, out.items);
|
||||
|
||||
while (try stmt.step()) {
|
||||
const row = try readLocalRecordRow(&stmt, gpa);
|
||||
errdefer freeLocalRecordRow(gpa, row);
|
||||
try out.append(gpa, row);
|
||||
}
|
||||
return out;
|
||||
return crud.listRows(LocalRecordRow, database, gpa, list_local_record_rows_sql, readLocalRecordRow);
|
||||
}
|
||||
|
||||
pub fn freeLocalRecordRow(gpa: Allocator, row: LocalRecordRow) void {
|
||||
gpa.free(row.name);
|
||||
gpa.free(row.value);
|
||||
crud.freeRow(LocalRecordRow, gpa, row);
|
||||
}
|
||||
|
||||
pub fn freeLocalRecordRows(gpa: Allocator, items: []const LocalRecordRow) void {
|
||||
for (items) |item| freeLocalRecordRow(gpa, item);
|
||||
crud.freeRows(LocalRecordRow, gpa, items);
|
||||
}
|
||||
|
||||
pub fn getLocalRecord(database: *db.Db, gpa: Allocator, id: i64) db.Error!?LocalRecordRow {
|
||||
@@ -232,28 +205,21 @@ pub const ForwardZoneRow = struct { id: i64, zone: []const u8, resolver: []const
|
||||
|
||||
/// Same order as `listForwardZones`; every string is a heap copy owned by `gpa`.
|
||||
pub fn listForwardZoneRows(database: *db.Db, gpa: Allocator) db.Error!std.ArrayList(ForwardZoneRow) {
|
||||
var stmt = try database.prepare("SELECT id, zone, resolver FROM forward_zones ORDER BY zone");
|
||||
defer stmt.deinit();
|
||||
|
||||
var out: std.ArrayList(ForwardZoneRow) = .empty;
|
||||
errdefer out.deinit(gpa);
|
||||
errdefer freeForwardZoneRows(gpa, out.items);
|
||||
|
||||
while (try stmt.step()) {
|
||||
const row = try readForwardZoneRow(&stmt, gpa);
|
||||
errdefer freeForwardZoneRow(gpa, row);
|
||||
try out.append(gpa, row);
|
||||
}
|
||||
return out;
|
||||
return crud.listRows(
|
||||
ForwardZoneRow,
|
||||
database,
|
||||
gpa,
|
||||
"SELECT id, zone, resolver FROM forward_zones ORDER BY zone",
|
||||
readForwardZoneRow,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn freeForwardZoneRow(gpa: Allocator, row: ForwardZoneRow) void {
|
||||
gpa.free(row.zone);
|
||||
gpa.free(row.resolver);
|
||||
crud.freeRow(ForwardZoneRow, gpa, row);
|
||||
}
|
||||
|
||||
pub fn freeForwardZoneRows(gpa: Allocator, items: []const ForwardZoneRow) void {
|
||||
for (items) |item| freeForwardZoneRow(gpa, item);
|
||||
crud.freeRows(ForwardZoneRow, gpa, items);
|
||||
}
|
||||
|
||||
pub fn getForwardZone(database: *db.Db, gpa: Allocator, id: i64) db.Error!?ForwardZoneRow {
|
||||
|
||||
@@ -37,34 +37,23 @@ const list_sql =
|
||||
|
||||
/// Every string in the result is a heap copy owned by `gpa`.
|
||||
pub fn listRules(database: *db.Db, gpa: Allocator) db.Error!std.ArrayList(model.Rule) {
|
||||
var stmt = try database.prepare(list_sql);
|
||||
defer stmt.deinit();
|
||||
return crud.listRows(model.Rule, database, gpa, list_sql, readRule);
|
||||
}
|
||||
|
||||
var out: std.ArrayList(model.Rule) = .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 freeRules(gpa, out.items);
|
||||
|
||||
while (try stmt.step()) {
|
||||
const group = try stmt.columnTextAlloc(gpa, 0);
|
||||
errdefer gpa.free(group);
|
||||
const pattern = try stmt.columnTextAlloc(gpa, 1);
|
||||
errdefer gpa.free(pattern);
|
||||
// The DDL's CHECK constraints make both decodes total for any row nxdns
|
||||
// wrote; `error.Mismatch` covers a row that something else wrote.
|
||||
const kind = model.RuleKind.fromDb(stmt.columnText(2)) orelse return error.Mismatch;
|
||||
const action = model.RuleAction.fromDb(stmt.columnText(3)) orelse return error.Mismatch;
|
||||
try out.append(gpa, .{ .group = group, .pattern = pattern, .kind = kind, .action = action });
|
||||
}
|
||||
return out;
|
||||
fn readRule(stmt: *db.Stmt, gpa: Allocator) db.Error!model.Rule {
|
||||
// The DDL's CHECK constraints make both decodes total for any row nxdns
|
||||
// wrote; `error.Mismatch` covers a row that something else wrote.
|
||||
const kind = model.RuleKind.fromDb(stmt.columnText(2)) orelse return error.Mismatch;
|
||||
const action = model.RuleAction.fromDb(stmt.columnText(3)) orelse return error.Mismatch;
|
||||
const group = try stmt.columnTextAlloc(gpa, 0);
|
||||
errdefer gpa.free(group);
|
||||
const pattern = try stmt.columnTextAlloc(gpa, 1);
|
||||
errdefer gpa.free(pattern);
|
||||
return .{ .group = group, .pattern = pattern, .kind = kind, .action = action };
|
||||
}
|
||||
|
||||
pub fn freeRules(gpa: Allocator, items: []const model.Rule) void {
|
||||
for (items) |item| {
|
||||
gpa.free(item.group);
|
||||
gpa.free(item.pattern);
|
||||
}
|
||||
crud.freeRows(model.Rule, gpa, items);
|
||||
}
|
||||
|
||||
const insert_sql =
|
||||
@@ -132,28 +121,15 @@ const get_rule_sql =
|
||||
|
||||
/// Same order as `listRules`; every string is a heap copy owned by `gpa`.
|
||||
pub fn listRuleRows(database: *db.Db, gpa: Allocator) db.Error!std.ArrayList(RuleRow) {
|
||||
var stmt = try database.prepare(list_rule_rows_sql);
|
||||
defer stmt.deinit();
|
||||
|
||||
var out: std.ArrayList(RuleRow) = .empty;
|
||||
errdefer out.deinit(gpa);
|
||||
errdefer freeRuleRows(gpa, out.items);
|
||||
|
||||
while (try stmt.step()) {
|
||||
const row = try readRuleRow(&stmt, gpa);
|
||||
errdefer freeRuleRow(gpa, row);
|
||||
try out.append(gpa, row);
|
||||
}
|
||||
return out;
|
||||
return crud.listRows(RuleRow, database, gpa, list_rule_rows_sql, readRuleRow);
|
||||
}
|
||||
|
||||
pub fn freeRuleRow(gpa: Allocator, row: RuleRow) void {
|
||||
gpa.free(row.group);
|
||||
gpa.free(row.pattern);
|
||||
crud.freeRow(RuleRow, gpa, row);
|
||||
}
|
||||
|
||||
pub fn freeRuleRows(gpa: Allocator, items: []const RuleRow) void {
|
||||
for (items) |item| freeRuleRow(gpa, item);
|
||||
crud.freeRows(RuleRow, gpa, items);
|
||||
}
|
||||
|
||||
pub fn getRule(database: *db.Db, gpa: Allocator, id: i64) db.Error!?RuleRow {
|
||||
|
||||
@@ -15,38 +15,34 @@ const db = @import("../db.zig");
|
||||
const migrations = @import("../migrations.zig");
|
||||
const model = @import("../../config/model.zig");
|
||||
const context = @import("context.zig");
|
||||
const crud = @import("crud.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();
|
||||
return crud.listRows(
|
||||
model.SettingPair,
|
||||
database,
|
||||
gpa,
|
||||
"SELECT key, value FROM settings ORDER BY key",
|
||||
readSetting,
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
fn readSetting(stmt: *db.Stmt, gpa: Allocator) db.Error!model.SettingPair {
|
||||
const key = try stmt.columnTextAlloc(gpa, 0);
|
||||
errdefer gpa.free(key);
|
||||
const value = try stmt.columnTextAlloc(gpa, 1);
|
||||
errdefer gpa.free(value);
|
||||
return .{ .key = key, .value = value };
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
crud.freeRows(model.SettingPair, gpa, items);
|
||||
}
|
||||
|
||||
pub fn insertSetting(database: *db.Db, item: model.SettingPair, ctx: InsertContext) db.Error!void {
|
||||
|
||||
@@ -25,35 +25,24 @@ const list_sql =
|
||||
|
||||
/// 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();
|
||||
return crud.listRows(model.BlocklistSource, database, gpa, list_sql, readBlocklistSource);
|
||||
}
|
||||
|
||||
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;
|
||||
fn readBlocklistSource(stmt: *db.Stmt, gpa: Allocator) db.Error!model.BlocklistSource {
|
||||
const url = try stmt.columnTextAlloc(gpa, 0);
|
||||
errdefer gpa.free(url);
|
||||
const name = try stmt.columnTextAlloc(gpa, 1);
|
||||
errdefer gpa.free(name);
|
||||
return .{
|
||||
.url = url,
|
||||
.name = name,
|
||||
.enabled = stmt.columnBool(2),
|
||||
.is_suggested = stmt.columnBool(3),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn freeBlocklistSources(gpa: Allocator, items: []const model.BlocklistSource) void {
|
||||
for (items) |item| {
|
||||
gpa.free(item.url);
|
||||
gpa.free(item.name);
|
||||
}
|
||||
crud.freeRows(model.BlocklistSource, gpa, items);
|
||||
}
|
||||
|
||||
const insert_sql =
|
||||
@@ -125,21 +114,7 @@ const list_rows_sql = row_columns_sql ++ " ORDER BY url";
|
||||
/// order `listBlocklistSources` uses. Every string is a heap copy owned by
|
||||
/// `gpa`; free the whole list with `freeSourceRows` and then `deinit` the list.
|
||||
pub fn listSourceRows(database: *db.Db, gpa: Allocator) db.Error!std.ArrayList(SourceRow) {
|
||||
var stmt = try database.prepare(list_rows_sql);
|
||||
defer stmt.deinit();
|
||||
|
||||
var out: std.ArrayList(SourceRow) = .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 freeSourceRows(gpa, out.items);
|
||||
|
||||
while (try stmt.step()) {
|
||||
const row = try readSourceRow(&stmt, gpa);
|
||||
errdefer freeSourceRow(gpa, row);
|
||||
try out.append(gpa, row);
|
||||
}
|
||||
return out;
|
||||
return crud.listRows(SourceRow, database, gpa, list_rows_sql, readSourceRow);
|
||||
}
|
||||
|
||||
fn readSourceRow(stmt: *db.Stmt, gpa: Allocator) db.Error!SourceRow {
|
||||
@@ -163,14 +138,13 @@ fn readSourceRow(stmt: *db.Stmt, gpa: Allocator) db.Error!SourceRow {
|
||||
};
|
||||
}
|
||||
|
||||
/// Frees `url`, `name` and the `checksum` payload when it is not null.
|
||||
pub fn freeSourceRow(gpa: Allocator, row: SourceRow) void {
|
||||
gpa.free(row.url);
|
||||
gpa.free(row.name);
|
||||
if (row.checksum) |value| gpa.free(value);
|
||||
crud.freeRow(SourceRow, gpa, row);
|
||||
}
|
||||
|
||||
pub fn freeSourceRows(gpa: Allocator, items: []const SourceRow) void {
|
||||
for (items) |item| freeSourceRow(gpa, item);
|
||||
crud.freeRows(SourceRow, gpa, items);
|
||||
}
|
||||
|
||||
const update_stats_sql =
|
||||
@@ -438,6 +412,12 @@ fn listSourceRowsUnderFailure(gpa: Allocator) !void {
|
||||
var rows = try listSourceRows(&database, gpa);
|
||||
defer rows.deinit(gpa);
|
||||
defer freeSourceRows(gpa, rows.items);
|
||||
|
||||
// `checksum` is the one allocated optional in this directory. Without a
|
||||
// non-null one in the result the injection never reaches its allocation and
|
||||
// this test stops covering the shape it exists for. Row id 1 sorts last:
|
||||
// `seedSources` inserts `c.example` first and the list orders by url.
|
||||
try testing.expect(rows.items[2].checksum != null);
|
||||
}
|
||||
|
||||
test "listSourceRows is leak-safe under allocation failure" {
|
||||
|
||||
@@ -21,38 +21,31 @@ 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(
|
||||
return crud.listRows(
|
||||
model.UpstreamServer,
|
||||
database,
|
||||
gpa,
|
||||
"SELECT url, priority, enabled, tls_name FROM upstreams ORDER BY priority, url",
|
||||
readUpstream,
|
||||
);
|
||||
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;
|
||||
const tls_name = try stmt.columnTextAlloc(gpa, 3);
|
||||
errdefer gpa.free(tls_name);
|
||||
try out.append(gpa, .{
|
||||
.url = url,
|
||||
.priority = priority,
|
||||
.enabled = stmt.columnBool(2),
|
||||
.tls_name = tls_name,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
fn readUpstream(stmt: *db.Stmt, gpa: Allocator) db.Error!model.UpstreamServer {
|
||||
const priority = std.math.cast(i32, stmt.columnInt(1)) orelse return error.Mismatch;
|
||||
const url = try stmt.columnTextAlloc(gpa, 0);
|
||||
errdefer gpa.free(url);
|
||||
const tls_name = try stmt.columnTextAlloc(gpa, 3);
|
||||
errdefer gpa.free(tls_name);
|
||||
return .{
|
||||
.url = url,
|
||||
.priority = priority,
|
||||
.enabled = stmt.columnBool(2),
|
||||
.tls_name = tls_name,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn freeUpstreams(gpa: Allocator, items: []const model.UpstreamServer) void {
|
||||
for (items) |item| {
|
||||
gpa.free(item.url);
|
||||
gpa.free(item.tls_name);
|
||||
}
|
||||
crud.freeRows(model.UpstreamServer, gpa, items);
|
||||
}
|
||||
|
||||
pub fn insertUpstream(database: *db.Db, item: model.UpstreamServer, ctx: InsertContext) db.Error!void {
|
||||
@@ -101,28 +94,15 @@ const get_upstream_sql =
|
||||
|
||||
/// Same order as `listUpstreams`; every string is a heap copy owned by `gpa`.
|
||||
pub fn listUpstreamRows(database: *db.Db, gpa: Allocator) db.Error!std.ArrayList(UpstreamRow) {
|
||||
var stmt = try database.prepare(list_upstream_rows_sql);
|
||||
defer stmt.deinit();
|
||||
|
||||
var out: std.ArrayList(UpstreamRow) = .empty;
|
||||
errdefer out.deinit(gpa);
|
||||
errdefer freeUpstreamRows(gpa, out.items);
|
||||
|
||||
while (try stmt.step()) {
|
||||
const row = try readUpstreamRow(&stmt, gpa);
|
||||
errdefer freeUpstreamRow(gpa, row);
|
||||
try out.append(gpa, row);
|
||||
}
|
||||
return out;
|
||||
return crud.listRows(UpstreamRow, database, gpa, list_upstream_rows_sql, readUpstreamRow);
|
||||
}
|
||||
|
||||
pub fn freeUpstreamRow(gpa: Allocator, row: UpstreamRow) void {
|
||||
gpa.free(row.url);
|
||||
gpa.free(row.tls_name);
|
||||
crud.freeRow(UpstreamRow, gpa, row);
|
||||
}
|
||||
|
||||
pub fn freeUpstreamRows(gpa: Allocator, items: []const UpstreamRow) void {
|
||||
for (items) |item| freeUpstreamRow(gpa, item);
|
||||
crud.freeRows(UpstreamRow, gpa, items);
|
||||
}
|
||||
|
||||
pub fn getUpstream(database: *db.Db, gpa: Allocator, id: i64) db.Error!?UpstreamRow {
|
||||
|
||||
Reference in New Issue
Block a user