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:
@@ -0,0 +1,89 @@
|
||||
//! The `querylog.db` schema version chain: comptime metadata and nothing else.
|
||||
//!
|
||||
//! Separate from `querylog_migrations.zig` so `tools/cut.zig` can import it
|
||||
//! without linking SQLite. Nothing in this file may reach for `db.zig`, for a
|
||||
//! C symbol, or for an allocator — the release gate reads these constants at
|
||||
//! build time, and a dependency here would drag the whole storage layer into
|
||||
//! the cut tool.
|
||||
//!
|
||||
//! **There are no migration hooks.** A step is a SQL file, period. Every
|
||||
//! shipped step is therefore byte-comparable against the previous tag, which is
|
||||
//! what lets the cut gate prove a released migration was never edited. A future
|
||||
//! change that genuinely cannot be expressed in SQL must amend this design in
|
||||
//! its own spec rather than adding a code path here.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// The version a file created by this build carries in `PRAGMA user_version`.
|
||||
pub const current_version: i32 = 1;
|
||||
|
||||
/// The oldest stamped version this build can reach `current_version` from.
|
||||
/// A file stamped below this refuses to open.
|
||||
///
|
||||
/// An EXPLICIT BREAK in a future release is expressed here and only here: bump
|
||||
/// `current_version`, set `minimum_supported_version = current_version`, and
|
||||
/// ship no step. The chain then cannot reach the new version from below the
|
||||
/// minimum, so `open` refuses the old file by the ordinary rules. A break is
|
||||
/// always versioned, always refused at runtime, and never silent.
|
||||
pub const minimum_supported_version: i32 = 1;
|
||||
|
||||
/// The literal `user_version` the 0.0.12 and 0.0.13 binaries stamped: the CRC32
|
||||
/// of their DDL text, under the pre-migration policy where a stamp mismatch
|
||||
/// meant "replace the file".
|
||||
///
|
||||
/// FROZEN. It is derived from nothing at build time on purpose — recomputing it
|
||||
/// from today's DDL would silently stop recognising the files it exists to
|
||||
/// recognise the moment the schema moves. Editing it strands every 0.0.12 and
|
||||
/// 0.0.13 file that has not yet been opened by a migration-aware build, which
|
||||
/// is why the cut gate fails on any change to this line.
|
||||
pub const legacy_fingerprint: i32 = 1975011655;
|
||||
|
||||
/// Logical versions live far below any plausible CRC32 stamp. A value above
|
||||
/// this is not a version this project ever wrote, so it classifies as
|
||||
/// unsupported rather than as a from-the-future schema.
|
||||
pub const version_floor_guard: i32 = 1_000_000;
|
||||
|
||||
/// One entry per shipped step: `step_sql[i]` migrates version
|
||||
/// `minimum_supported_version + i` to `minimum_supported_version + i + 1`.
|
||||
/// Each entry is `@embedFile("migrations/v<from>.sql")`, and each such file is
|
||||
/// immutable once released.
|
||||
///
|
||||
/// **Step-authoring rules** (the runner enforces the first, the equivalence
|
||||
/// oracle catches violations of the rest):
|
||||
///
|
||||
/// - A step contains no transaction statement. No `BEGIN`, no `COMMIT`, no
|
||||
/// `ROLLBACK`, no `SAVEPOINT`: the runner wraps the whole chain in one
|
||||
/// transaction and installs an authorizer that denies them outright.
|
||||
/// - A step that changes a table's shape must REBUILD it, so that the CREATE
|
||||
/// text SQLite stores ends up byte-identical to the fresh DDL's:
|
||||
/// `DROP` every view over `<t>` first; `ALTER TABLE <t> RENAME TO <t>_old`;
|
||||
/// `CREATE TABLE <t> ...` pasted verbatim from `querylog_schema.ddl`;
|
||||
/// `INSERT INTO <t> SELECT ... FROM <t>_old`; `DROP TABLE <t>_old`; recreate
|
||||
/// every index and trigger of `<t>` verbatim; recreate the dropped views
|
||||
/// verbatim last.
|
||||
/// - `ALTER TABLE ... ADD COLUMN` and `ALTER TABLE ... RENAME COLUMN` on a kept
|
||||
/// table are forbidden. SQLite rewrites the stored CREATE text under them,
|
||||
/// and the oracle's exact-text layer would rightly call the result unequal.
|
||||
pub const step_sql: []const [:0]const u8 = &.{};
|
||||
|
||||
comptime {
|
||||
std.debug.assert(minimum_supported_version >= 1);
|
||||
std.debug.assert(minimum_supported_version <= current_version);
|
||||
std.debug.assert(current_version <= version_floor_guard);
|
||||
std.debug.assert(legacy_fingerprint < 0 or legacy_fingerprint > version_floor_guard);
|
||||
std.debug.assert(step_sql.len == @as(usize, @intCast(current_version - minimum_supported_version)));
|
||||
}
|
||||
|
||||
test "the chain covers exactly the supported range" {
|
||||
try std.testing.expectEqual(
|
||||
@as(usize, @intCast(current_version - minimum_supported_version)),
|
||||
step_sql.len,
|
||||
);
|
||||
}
|
||||
|
||||
test "the legacy anchor is the literal 0.0.12 stamp" {
|
||||
// Not `fingerprintOf(ddl)`. The number is a historical fact about released
|
||||
// binaries, so a test that recomputed it would move with the schema and
|
||||
// prove nothing.
|
||||
try std.testing.expectEqual(@as(i32, 1975011655), legacy_fingerprint);
|
||||
}
|
||||
Reference in New Issue
Block a user