use std::fmt;
use serde::{Deserialize, Serialize};
use super::error::VersionError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SchemaVersion(pub u32);
impl SchemaVersion {
pub const fn new(v: u32) -> Self {
Self(v)
}
pub fn check_can_apply(
node_max: SchemaVersion,
event_required: SchemaVersion,
table: &'static str,
) -> Result<(), VersionError> {
if event_required > node_max {
return Err(VersionError::SchemaTooNew {
table,
node_max,
event_required,
});
}
Ok(())
}
}
impl fmt::Display for SchemaVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "v{}", self.0)
}
}
impl From<u32> for SchemaVersion {
fn from(v: u32) -> Self {
Self(v)
}
}
impl From<SchemaVersion> for u32 {
fn from(s: SchemaVersion) -> Self {
s.0
}
}
pub const TABLE_SCHEMA_VERSIONS: &[(&str, SchemaVersion)] = &[
("memory_commit_log", SchemaVersion(1)),
("quota_policies", SchemaVersion(1)),
("quota_audit_log", SchemaVersion(1)),
("memory_tombstones", SchemaVersion(1)),
("hnsw_manifests", SchemaVersion(1)),
("backup_manifests", SchemaVersion(1)),
("durable_jobs", SchemaVersion(1)),
("tenant_config_overrides", SchemaVersion(1)),
];
pub fn expected_schema_version(table: &str) -> Option<SchemaVersion> {
TABLE_SCHEMA_VERSIONS
.iter()
.find(|(name, _)| *name == table)
.map(|(_, ver)| *ver)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_can_apply_accepts_same_or_older() {
let node = SchemaVersion::new(5);
assert!(SchemaVersion::check_can_apply(node, SchemaVersion::new(1), "t").is_ok());
assert!(SchemaVersion::check_can_apply(node, SchemaVersion::new(5), "t").is_ok());
}
#[test]
fn check_can_apply_rejects_newer_with_structured_error() {
let node = SchemaVersion::new(5);
let event = SchemaVersion::new(7);
let err = SchemaVersion::check_can_apply(node, event, "memory_commit_log").unwrap_err();
match err {
VersionError::SchemaTooNew {
table,
node_max,
event_required,
} => {
assert_eq!(table, "memory_commit_log");
assert_eq!(node_max, node);
assert_eq!(event_required, event);
}
other => panic!("wrong error variant: {other:?}"),
}
}
#[test]
fn ordering_is_natural() {
assert!(SchemaVersion::new(1) < SchemaVersion::new(2));
assert!(SchemaVersion::new(99) < SchemaVersion::new(100));
}
#[test]
fn expected_schema_version_finds_known_tables() {
assert!(expected_schema_version("memory_commit_log").is_some());
assert!(expected_schema_version("quota_policies").is_some());
assert!(expected_schema_version("hnsw_manifests").is_some());
}
#[test]
fn expected_schema_version_returns_none_for_unknown() {
assert_eq!(expected_schema_version("does_not_exist"), None);
}
#[test]
fn table_names_in_registry_are_unique() {
let mut seen = std::collections::HashSet::new();
for (name, _) in TABLE_SCHEMA_VERSIONS {
assert!(
seen.insert(*name),
"duplicate entry in TABLE_SCHEMA_VERSIONS: {name}"
);
}
}
#[test]
fn serde_uses_compact_integer_form() {
let v = SchemaVersion::new(42);
let json = serde_json::to_string(&v).unwrap();
assert_eq!(json, "42");
}
}