pub const SEGMENT_FORMAT_VERSION: u16 = 1;
pub const WAL_FORMAT_VERSION: u16 = 1;
pub const CATALOG_FORMAT_VERSION: u32 = 1;
pub const SERIES_INDEX_FORMAT_VERSION: u8 = 1;
#[must_use]
pub fn version_mismatch(what: &str, found: u64, expected: u64) -> String {
format!(
"{what} is format version {found}, and this build of chronix reads \
version {expected}. Chronix has no on-disk migration: a data \
directory belongs to one format generation. Either run the version \
of chronix that wrote it, or start a new data directory and re-ingest \
— see the release notes for the version that changed it."
)
}
#[must_use]
pub fn check(what: &str, found: u64, expected: u64) -> Option<String> {
(found != expected).then(|| version_mismatch(what, found, expected))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_exact_match_passes() {
assert!(check("a segment", 2, 2).is_none());
}
#[test]
fn an_older_version_is_refused_as_firmly_as_a_newer_one() {
assert!(check("a catalog", 1, 2).is_some());
assert!(check("a catalog", 3, 2).is_some());
}
#[test]
fn the_message_names_the_remedy_not_just_the_number() {
let m = check("the WAL", 1, 2).expect("mismatch");
assert!(m.contains("version 1"), "{m}");
assert!(m.contains("version 2"), "{m}");
assert!(
m.contains("re-ingest") || m.contains("new data directory"),
"a version mismatch is the first error an operator meets; it has \
to say what to do: {m}"
);
}
#[test]
fn every_durable_format_is_on_the_same_generation() {
let g = u64::from(SEGMENT_FORMAT_VERSION);
assert_eq!(u64::from(WAL_FORMAT_VERSION), g);
assert_eq!(u64::from(CATALOG_FORMAT_VERSION), g);
assert_eq!(u64::from(SERIES_INDEX_FORMAT_VERSION), g);
}
}
#[cfg(test)]
mod policy_tests {
#[test]
fn no_durable_reader_reports_a_version_mismatch_as_corruption() {
for (name, src) in [
("wal/reader.rs", include_str!("wal/reader.rs")),
(
"index/series_index.rs",
include_str!("index/series_index.rs"),
),
("index/catalog.rs", include_str!("index/catalog.rs")),
("segment/header.rs", include_str!("segment/header.rs")),
] {
let Some(at) = src.find("format::check").or_else(|| src.find("!= VERSION")) else {
panic!("{name}: no format-version check found — did the reader stop checking?");
};
let tail = &src[at..];
let end = tail
.find("return Err(")
.and_then(|i| tail[i..].find(';').map(|j| i + j + 1))
.unwrap_or(tail.len());
let window: String = tail[..end]
.lines()
.map(|l| l.split("//").next().unwrap_or(""))
.collect::<Vec<_>>()
.join("\n");
assert!(
!window.contains("Corrupt"),
"{name} reports a format-version mismatch as corruption. The \
bytes are intact and the reader is from another generation; \
saying `Corrupt` sends an operator to a restore when the fix \
is to match the versions up."
);
}
}
}