Files
nxdns/src/storage/querylog_versions.zig
T
mokhtar 272655f60c 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.
2026-08-28 17:56:19 +02:00

90 lines
4.5 KiB
Zig

//! 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);
}