storage: version querylog.db and migrate it in place, never reset a healthy file

querylog.db carries a schema version; migrations run at startup as one transaction after a vacuumed 0600 backup, and every failure refuses startup (exit 2, no systemd restart loop) instead of starting empty. corruption is the only automatic recreate left. the cut gate now requires a fixture-proven migration or an explicit versioned break with restore instructions, and locks shipped migration files and fixtures byte-for-byte.
This commit is contained in:
2026-08-28 17:56:19 +02:00
parent dd5a9f6ab8
commit 272655f60c
25 changed files with 4312 additions and 176 deletions
+25 -1
View File
@@ -13,7 +13,7 @@ const validate = @import("validate.zig");
/// `ValidateError` enters as a whole set rather than variant by variant, so a
/// variant added to the validator cannot silently fall through to exit 1. The
/// five extras are the configuration faults raised outside the validator: the
/// first five extras are the configuration faults raised outside the validator: the
/// ZON reader (`ParseZon`), the file size limit (`ConfigTooLarge`), the managed
/// file the operator named and this process cannot open
/// (`ManagedConfigUnreadable`, milestone-20 ruling 2), the composition root's
@@ -26,6 +26,16 @@ const validate = @import("validate.zig");
/// missing file anywhere else stays a runtime failure. `config/loader.zig` owns
/// the conversion and the closed set of open errors that qualify.
///
/// The four querylog schema refusals are here for the exit code, not because a
/// `.zon` file is wrong: `SchemaTooNew` and `SchemaUnsupported` are a deliberate
/// refusal to touch a `querylog.db` this binary does not understand, and
/// `MigrationFailed` and `MigrationBackupFailed` are a deliberate refusal to run
/// on a database whose migration or pre-migration backup did not complete. All
/// four need an operator, and none of them will resolve on a retry — exit 1 puts
/// them under systemd's `Restart=on-failure` and restart-loops a server that is
/// refusing on purpose. The unit's `RestartPreventExitStatus=2 64` is what exit
/// 2 buys them.
///
/// Not here on purpose: `error.DestructiveImport`, which reports what an import
/// would do to the database rather than the content of a file, and is the one
/// config-shaped exit 2 `cli` decides for itself.
@@ -35,6 +45,10 @@ const ConfigFault = validate.ValidateError || error{
ManagedConfigUnreadable,
NoUsableUpstreams,
BadCertificate,
SchemaTooNew,
SchemaUnsupported,
MigrationFailed,
MigrationBackupFailed,
};
const faults: []const anyerror = blk: {
@@ -119,6 +133,16 @@ test "the seed-file errors that used to exit 1 from run are configuration faults
try testing.expect(isConfigFault(error.NoUpstreams));
}
test "the querylog schema refusals exit 2 so systemd does not restart-loop them" {
try testing.expect(isConfigFault(error.SchemaTooNew));
try testing.expect(isConfigFault(error.SchemaUnsupported));
try testing.expect(isConfigFault(error.MigrationFailed));
try testing.expect(isConfigFault(error.MigrationBackupFailed));
// The refusals are a closed set. A neighbouring schema error is a corrupt
// database, not a refusal, and stays a runtime failure.
try testing.expect(!isConfigFault(error.SchemaCorrupt));
}
test "a runtime failure is not a configuration fault" {
try testing.expect(!isConfigFault(error.OutOfMemory));
try testing.expect(!isConfigFault(error.AccessDenied));