#[cfg(feature = "graph")]
use grafeo::GrafeoDB;
pub const CURRENT_SCHEMA_VERSION: u32 = 2;
#[derive(Debug, PartialEq)]
pub enum VersionCheck {
UpToDate,
NeedsMigration { from: u32, to: u32 },
CodeBehind {
graph_version: u32,
code_version: u32,
},
}
mod queries {
pub const READ_VERSION: &str = "MATCH (v:SchemaVersion) RETURN v.version";
pub fn insert_version(version: u32, now: &str, code_ver: &str) -> String {
format!(
"INSERT (:SchemaVersion {{singleton: 'schema_version', version: {}, migrated_at: '{}', code_version: '{}'}})",
version, now, code_ver
)
}
pub const MATCH_ALL_MEMORIES: &str = "MATCH (m:Memory) RETURN m.id, m.created_at";
}
#[cfg(feature = "graph")]
pub fn check_version(db: &GrafeoDB) -> VersionCheck {
let session = db.session();
match session.execute(queries::READ_VERSION) {
Ok(result) => {
if let Some(row) = result.iter().next() {
let graph_ver = row
.first()
.and_then(|v| format!("{}", v).parse::<u32>().ok())
.unwrap_or(0);
if graph_ver == CURRENT_SCHEMA_VERSION {
VersionCheck::UpToDate
} else if graph_ver < CURRENT_SCHEMA_VERSION {
VersionCheck::NeedsMigration {
from: graph_ver,
to: CURRENT_SCHEMA_VERSION,
}
} else {
VersionCheck::CodeBehind {
graph_version: graph_ver,
code_version: CURRENT_SCHEMA_VERSION,
}
}
} else {
VersionCheck::NeedsMigration {
from: 0,
to: CURRENT_SCHEMA_VERSION,
}
}
}
Err(_) => {
VersionCheck::NeedsMigration {
from: 0,
to: CURRENT_SCHEMA_VERSION,
}
}
}
}
#[cfg(not(feature = "graph"))]
pub fn check_version(_db: &()) -> VersionCheck {
VersionCheck::UpToDate
}
#[cfg(feature = "graph")]
pub fn run_migrations(db: &GrafeoDB, from: u32, to: u32) -> cersei_types::Result<()> {
tracing::info!("Migrating graph schema from v{} to v{}", from, to);
let mut current = from;
while current < to {
match current {
0 => migrate_v0_to_v1(db)?,
1 => migrate_v1_to_v2(db)?,
_ => {
return Err(cersei_types::CerseiError::Config(format!(
"Unknown migration: v{} → v{}",
current,
current + 1
)));
}
}
current += 1;
}
stamp_version(db, to)?;
tracing::info!("Graph schema migration complete: v{}", to);
Ok(())
}
#[cfg(not(feature = "graph"))]
pub fn run_migrations(_db: &(), _from: u32, _to: u32) -> cersei_types::Result<()> {
Ok(())
}
#[cfg(feature = "graph")]
fn stamp_version(db: &GrafeoDB, version: u32) -> cersei_types::Result<()> {
let session = db.session();
let now = chrono::Utc::now().to_rfc3339();
let code_ver = env!("CARGO_PKG_VERSION");
let _ = session.execute("MATCH (v:SchemaVersion) DELETE v");
session
.execute(&queries::insert_version(version, &now, code_ver))
.map_err(|e| {
cersei_types::CerseiError::Config(format!("Failed to stamp schema version: {}", e))
})?;
Ok(())
}
#[cfg(feature = "graph")]
fn migrate_v0_to_v1(db: &GrafeoDB) -> cersei_types::Result<()> {
tracing::debug!("Running migration v0 → v1 (stamp version, no data changes)");
Ok(())
}
#[cfg(feature = "graph")]
fn migrate_v1_to_v2(db: &GrafeoDB) -> cersei_types::Result<()> {
tracing::debug!("Running migration v1 → v2 (add decay/embedding fields)");
Ok(())
}
pub fn effective_confidence(base: f32, decay_rate: f32, last_validated_at: &str) -> f32 {
if last_validated_at.is_empty() || decay_rate <= 0.0 {
return base.clamp(0.0, 1.0);
}
let validated = match chrono::DateTime::parse_from_rfc3339(last_validated_at) {
Ok(dt) => dt.with_timezone(&chrono::Utc),
Err(_) => return base.clamp(0.0, 1.0),
};
let days = (chrono::Utc::now() - validated).num_days().max(0) as f32;
(base - decay_rate * days).clamp(0.0, 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_effective_confidence_no_decay() {
assert_eq!(effective_confidence(0.9, 0.0, ""), 0.9);
assert_eq!(effective_confidence(0.9, 0.0, "2024-01-01T00:00:00Z"), 0.9);
}
#[test]
fn test_effective_confidence_with_decay() {
let old = "2020-01-01T00:00:00Z";
let result = effective_confidence(0.9, 0.01, old);
assert_eq!(result, 0.0);
}
#[test]
fn test_effective_confidence_recent() {
let now = chrono::Utc::now().to_rfc3339();
let result = effective_confidence(0.9, 0.01, &now);
assert!((result - 0.9).abs() < 0.02);
}
#[test]
fn test_effective_confidence_invalid_date() {
assert_eq!(effective_confidence(0.8, 0.01, "not-a-date"), 0.8);
}
#[test]
fn test_effective_confidence_clamps() {
assert_eq!(effective_confidence(1.5, 0.0, ""), 1.0);
assert_eq!(effective_confidence(-0.5, 0.0, ""), 0.0);
}
#[cfg(feature = "graph")]
#[test]
fn test_check_version_fresh_graph() {
let db = GrafeoDB::new_in_memory();
let check = check_version(&db);
assert_eq!(
check,
VersionCheck::NeedsMigration {
from: 0,
to: CURRENT_SCHEMA_VERSION
}
);
}
#[cfg(feature = "graph")]
#[test]
fn test_migration_and_recheck() {
let db = GrafeoDB::new_in_memory();
let check = check_version(&db);
assert_eq!(check, VersionCheck::NeedsMigration { from: 0, to: 2 });
run_migrations(&db, 0, 2).unwrap();
let check = check_version(&db);
assert_eq!(check, VersionCheck::UpToDate);
}
#[cfg(feature = "graph")]
#[test]
fn test_migration_idempotent() {
let db = GrafeoDB::new_in_memory();
run_migrations(&db, 0, 2).unwrap();
run_migrations(&db, 0, 2).unwrap();
let check = check_version(&db);
assert_eq!(check, VersionCheck::UpToDate);
}
}