use saya_types::SchemaTree;
use std::time::{SystemTime, UNIX_EPOCH};
pub(crate) const MODEL_SCHEMA_MAX_AGE_MS: i64 = 24 * 60 * 60 * 1000;
pub(crate) fn now_unix_ms() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|value| value.as_millis() as i64)
.unwrap_or_default()
}
#[derive(Debug, Clone)]
pub(crate) enum SchemaAvailability {
Available {
schema: SchemaTree,
observed_at_unix_ms: i64,
},
Missing,
Unavailable,
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum SchemaFreshness {
Bounded { now_unix_ms: i64, max_age_ms: i64 },
Unbounded,
}
impl SchemaFreshness {
pub(crate) fn for_model(now_unix_ms: i64) -> Self {
Self::Bounded {
now_unix_ms,
max_age_ms: MODEL_SCHEMA_MAX_AGE_MS,
}
}
}
impl SchemaAvailability {
pub(crate) fn available(schema: SchemaTree, observed_at_unix_ms: i64) -> Self {
Self::Available {
schema,
observed_at_unix_ms,
}
}
pub(crate) fn live_table_schema(&self, freshness: SchemaFreshness) -> Option<&SchemaTree> {
let Self::Available {
schema,
observed_at_unix_ms,
} = self
else {
return None;
};
match freshness {
SchemaFreshness::Unbounded => Some(schema),
SchemaFreshness::Bounded {
now_unix_ms,
max_age_ms,
} => {
if now_unix_ms.saturating_sub(*observed_at_unix_ms) <= max_age_ms {
Some(schema)
} else {
None
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tree() -> SchemaTree {
SchemaTree::default()
}
#[test]
fn missing_yields_no_live_schema_under_any_freshness() {
assert!(
SchemaAvailability::Missing
.live_table_schema(SchemaFreshness::Unbounded)
.is_none()
);
assert!(
SchemaAvailability::Missing
.live_table_schema(SchemaFreshness::for_model(0))
.is_none()
);
}
#[test]
fn unavailable_yields_no_live_schema_under_any_freshness() {
assert!(
SchemaAvailability::Unavailable
.live_table_schema(SchemaFreshness::Unbounded)
.is_none()
);
assert!(
SchemaAvailability::Unavailable
.live_table_schema(SchemaFreshness::for_model(0))
.is_none()
);
}
#[test]
fn available_within_bound_yields_the_schema() {
let one_hour = 60 * 60 * 1000;
let avail = SchemaAvailability::available(tree(), 0);
let schema = avail
.live_table_schema(SchemaFreshness::for_model(one_hour))
.expect("fresh cache classifies against its schema");
assert!(schema.databases.is_empty());
}
#[test]
fn available_older_than_the_bound_yields_nothing_for_the_model_path() {
let now = 25 * 60 * 60 * 1000;
let avail = SchemaAvailability::available(tree(), 0);
assert!(
avail
.live_table_schema(SchemaFreshness::for_model(now))
.is_none(),
"a cache older than the bound must not classify Current"
);
}
#[test]
fn available_older_than_the_bound_still_classifies_for_a_human() {
let avail = SchemaAvailability::available(tree(), 0);
assert!(
avail
.live_table_schema(SchemaFreshness::Unbounded)
.is_some(),
"human path uses a stale-by-age cache to show what it knows"
);
}
#[test]
fn cache_exactly_at_the_bound_is_still_fresh() {
let max_age = MODEL_SCHEMA_MAX_AGE_MS;
let avail = SchemaAvailability::available(tree(), 0);
assert!(
avail
.live_table_schema(SchemaFreshness::for_model(max_age))
.is_some(),
"a cache exactly at the bound is fresh, not stale-by-age"
);
}
}