storage: version querylog.db and migrate it in place, never reset a healthy file
Gates / frontend (push) Successful in 2m8s
Gates / test (push) Successful in 2m46s
Gates / test-aarch64 (push) Successful in 8m38s
Gates / package (push) Successful in 4m39s
Gates / container (push) Successful in 15s
CI / gates (push) Successful in 31m58s
Gates / frontend (push) Successful in 2m8s
Gates / test (push) Successful in 2m46s
Gates / test-aarch64 (push) Successful in 8m38s
Gates / package (push) Successful in 4m39s
Gates / container (push) Successful in 15s
CI / gates (push) Successful in 31m58s
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:
@@ -28,7 +28,9 @@ const model = @import("../config/model.zig");
|
||||
const validate = @import("../config/validate.zig");
|
||||
const db = @import("db.zig");
|
||||
const migrations = @import("migrations.zig");
|
||||
const querylog_migrations = @import("querylog_migrations.zig");
|
||||
const querylog_schema = @import("querylog_schema.zig");
|
||||
const querylog_versions = @import("querylog_versions.zig");
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
@@ -354,7 +356,7 @@ test "S7 case 1: querylog open on a fresh directory creates the schema" {
|
||||
|
||||
try testing.expectEqual(querylog_schema.RecreateReason.missing, result.recreated.?);
|
||||
try testing.expectEqual(
|
||||
@as(i64, querylog_schema.fingerprint),
|
||||
@as(i64, querylog_versions.current_version),
|
||||
try result.database.queryInt("PRAGMA user_version"),
|
||||
);
|
||||
try testing.expectEqual(
|
||||
@@ -387,7 +389,7 @@ test "S7 case 2: reopening a healthy querylog recreates nothing" {
|
||||
try testing.expectEqual(@as(usize, 0), asides.items.items.len);
|
||||
}
|
||||
|
||||
test "S7 case 3: a wrong user_version recreates and keeps the old file aside" {
|
||||
test "S7 case 3: a version this build cannot handle refuses and touches nothing" {
|
||||
if (!build_options.integration) return error.SkipZigTest;
|
||||
|
||||
var f: Fixture = .init();
|
||||
@@ -396,29 +398,42 @@ test "S7 case 3: a wrong user_version recreates and keeps the old file aside" {
|
||||
|
||||
var buf: [path_buf_len]u8 = undefined;
|
||||
const path = try f.pathZ(&buf, "querylog.db");
|
||||
try stampUserVersion(path, querylog_schema.fingerprint +% 1);
|
||||
|
||||
const original = try f.read("querylog.db");
|
||||
defer testing.allocator.free(original);
|
||||
querylog_migrations.expected_failures.begin();
|
||||
defer querylog_migrations.expected_failures.end();
|
||||
|
||||
var result = try querylog_schema.open(io, std.Io.Dir.cwd(), path);
|
||||
defer result.database.close();
|
||||
try testing.expectEqual(
|
||||
querylog_schema.RecreateReason.fingerprint_mismatch,
|
||||
result.recreated.?,
|
||||
);
|
||||
// Both refusal lanes, against a checkpointed file with no sidecars beside
|
||||
// it: a stamp above this build's version (a downgrade) and a stamp that is
|
||||
// not a version at all (a pre-0.0.12 fingerprint, or a foreign file).
|
||||
const refusals = [_]struct { stamp: i32, expected: anyerror }{
|
||||
.{ .stamp = querylog_versions.current_version + 1, .expected = error.SchemaTooNew },
|
||||
.{ .stamp = 0, .expected = error.SchemaUnsupported },
|
||||
.{ .stamp = -7, .expected = error.SchemaUnsupported },
|
||||
.{ .stamp = 603440875, .expected = error.SchemaUnsupported },
|
||||
};
|
||||
|
||||
var asides = try collectAsides(&f);
|
||||
defer asides.deinit();
|
||||
try testing.expectEqual(@as(usize, 1), asides.items.items.len);
|
||||
for (refusals) |lane| {
|
||||
try stampUserVersion(path, lane.stamp);
|
||||
try testing.expect(!try f.exists("querylog.db-wal"));
|
||||
|
||||
// The file was healthy: this build's schema moved, the database did not rot.
|
||||
// An operator who reads "corrupt" here deletes a file that was never broken.
|
||||
try testing.expect(std.mem.startsWith(u8, asides.items.items[0], "querylog.db.schema-changed-"));
|
||||
const original = try f.read("querylog.db");
|
||||
defer testing.allocator.free(original);
|
||||
|
||||
const kept = try f.read(asides.items.items[0]);
|
||||
defer testing.allocator.free(kept);
|
||||
try testing.expectEqualSlices(u8, original, kept);
|
||||
try testing.expectError(
|
||||
lane.expected,
|
||||
querylog_schema.open(io, std.Io.Dir.cwd(), path),
|
||||
);
|
||||
|
||||
// Byte-identical, not merely "still readable": nothing was rewritten,
|
||||
// no aside was made, and no fresh database was created beside it.
|
||||
const after = try f.read("querylog.db");
|
||||
defer testing.allocator.free(after);
|
||||
try testing.expectEqualSlices(u8, original, after);
|
||||
|
||||
var asides = try collectAsides(&f);
|
||||
defer asides.deinit();
|
||||
try testing.expectEqual(@as(usize, 0), asides.items.items.len);
|
||||
}
|
||||
}
|
||||
|
||||
test "S7 case 4: a garbage file recreates and the garbage is preserved" {
|
||||
@@ -468,11 +483,11 @@ test "S7 case 5: two recreates in the same second produce two distinct aside fil
|
||||
|
||||
var round: usize = 0;
|
||||
while (round < 2) : (round += 1) {
|
||||
try stampUserVersion(path, querylog_schema.fingerprint +% 1);
|
||||
try f.write("querylog.db", "not a database at all");
|
||||
var result = try querylog_schema.open(io, std.Io.Dir.cwd(), path);
|
||||
defer result.database.close();
|
||||
try testing.expectEqual(
|
||||
querylog_schema.RecreateReason.fingerprint_mismatch,
|
||||
querylog_schema.RecreateReason.not_a_database,
|
||||
result.recreated.?,
|
||||
);
|
||||
}
|
||||
@@ -492,7 +507,7 @@ test "S7 case 6: a stale write-ahead log is removed before the fresh database is
|
||||
|
||||
var buf: [path_buf_len]u8 = undefined;
|
||||
const path = try f.pathZ(&buf, "querylog.db");
|
||||
try stampUserVersion(path, querylog_schema.fingerprint +% 1);
|
||||
try f.write("querylog.db", "not a database at all");
|
||||
|
||||
// Existence alone proves nothing: the fresh database turns WAL on again and
|
||||
// writes its own `-wal`. The marker is what distinguishes the stale file
|
||||
@@ -503,7 +518,7 @@ test "S7 case 6: a stale write-ahead log is removed before the fresh database is
|
||||
var result = try querylog_schema.open(io, std.Io.Dir.cwd(), path);
|
||||
defer result.database.close();
|
||||
try testing.expectEqual(
|
||||
querylog_schema.RecreateReason.fingerprint_mismatch,
|
||||
querylog_schema.RecreateReason.not_a_database,
|
||||
result.recreated.?,
|
||||
);
|
||||
|
||||
@@ -591,7 +606,7 @@ test "S7 case 23: a locked querylog propagates Busy and is never destroyed" {
|
||||
defer reopened.database.close();
|
||||
try testing.expectEqual(@as(?querylog_schema.RecreateReason, null), reopened.recreated);
|
||||
try testing.expectEqual(
|
||||
@as(i64, querylog_schema.fingerprint),
|
||||
@as(i64, querylog_versions.current_version),
|
||||
try reopened.database.queryInt("PRAGMA user_version"),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user