use crate::core::id::VersionId;
use crate::core::interning::InternedString;
use crate::core::property::{PropertyMap, PropertyValue};
use crate::core::temporal::{BiTemporalInterval, Timestamp};
#[derive(Debug, Clone, PartialEq)]
pub struct VersionInfo {
pub version_number: u64,
pub version_id: VersionId,
pub temporal: BiTemporalInterval,
pub properties: PropertyMap,
pub label: String,
}
#[derive(Debug, Clone)]
pub struct EntityHistory {
pub versions: Vec<VersionInfo>,
}
impl EntityHistory {
#[must_use]
pub fn version_count(&self) -> usize {
self.versions.len()
}
#[must_use]
pub fn current_version(&self) -> Option<&VersionInfo> {
self.versions.last()
}
#[must_use]
pub fn first_version(&self) -> Option<&VersionInfo> {
self.versions.first()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct VersionDiff {
pub from_version: VersionId,
pub to_version: VersionId,
pub added: PropertyMap,
pub removed: PropertyMap,
pub modified: Vec<(InternedString, PropertyValue, PropertyValue)>,
}
impl VersionDiff {
#[must_use]
pub fn compute(
from: &PropertyMap,
to: &PropertyMap,
from_id: VersionId,
to_id: VersionId,
) -> Self {
use crate::core::property::PropertyMapBuilder;
let mut added_builder = PropertyMapBuilder::new();
let mut removed_builder = PropertyMapBuilder::new();
let mut modified = Vec::new();
for (key, to_value) in to.iter() {
match from.get_by_interned_key(key) {
None => {
added_builder = added_builder.insert_by_key(*key, to_value.clone());
}
Some(from_value) if !from_value.semantically_equal(to_value) => {
modified.push((*key, from_value.clone(), to_value.clone()));
}
_ => {
}
}
}
for (key, from_value) in from.iter() {
if !to.contains_interned_key(key) {
removed_builder = removed_builder.insert_by_key(*key, from_value.clone());
}
}
VersionDiff {
from_version: from_id,
to_version: to_id,
added: added_builder.build(),
removed: removed_builder.build(),
modified,
}
}
#[must_use]
pub fn has_changes(&self) -> bool {
!self.added.is_empty() || !self.removed.is_empty() || !self.modified.is_empty()
}
#[must_use]
pub fn change_count(&self) -> usize {
self.added.len() + self.removed.len() + self.modified.len()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct VersionSummary {
pub version_id: VersionId,
pub version_number: u64,
pub valid_from: Timestamp,
pub transaction_time: Timestamp,
pub properties_added: usize,
pub properties_removed: usize,
pub properties_modified: usize,
}
impl VersionSummary {
#[must_use]
pub fn has_changes(&self) -> bool {
self.properties_added > 0 || self.properties_removed > 0 || self.properties_modified > 0
}
#[must_use]
pub fn change_count(&self) -> usize {
self.properties_added + self.properties_removed + self.properties_modified
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::GLOBAL_INTERNER;
use crate::core::hlc::HybridTimestamp;
use crate::core::property::PropertyMapBuilder;
fn test_timestamp(wallclock: i64) -> Timestamp {
HybridTimestamp::new(wallclock, 0).unwrap()
}
fn test_version_id(id: u64) -> VersionId {
VersionId::new(id).unwrap()
}
#[test]
fn test_version_diff_compute_detects_added() {
let from_props = PropertyMapBuilder::new().insert("name", "Alice").build();
let to_props = PropertyMapBuilder::new()
.insert("name", "Alice")
.insert("age", 30i64)
.build();
let diff = VersionDiff::compute(
&from_props,
&to_props,
test_version_id(1),
test_version_id(2),
);
assert_eq!(diff.added.len(), 1);
assert!(diff.added.contains_key("age"));
assert!(diff.removed.is_empty());
assert!(diff.modified.is_empty());
}
#[test]
fn test_version_diff_compute_detects_removed() {
let from_props = PropertyMapBuilder::new()
.insert("name", "Alice")
.insert("age", 30i64)
.build();
let to_props = PropertyMapBuilder::new().insert("name", "Alice").build();
let diff = VersionDiff::compute(
&from_props,
&to_props,
test_version_id(1),
test_version_id(2),
);
assert!(diff.added.is_empty());
assert_eq!(diff.removed.len(), 1);
assert!(diff.removed.contains_key("age"));
assert!(diff.modified.is_empty());
}
#[test]
fn test_version_diff_compute_detects_modified() {
let from_props = PropertyMapBuilder::new().insert("name", "Alice").build();
let to_props = PropertyMapBuilder::new().insert("name", "Bob").build();
let diff = VersionDiff::compute(
&from_props,
&to_props,
test_version_id(1),
test_version_id(2),
);
assert!(diff.added.is_empty());
assert!(diff.removed.is_empty());
assert_eq!(diff.modified.len(), 1);
use crate::core::GLOBAL_INTERNER;
let key_str = GLOBAL_INTERNER
.resolve_with(diff.modified[0].0, |s| s.to_string())
.unwrap();
assert_eq!(key_str, "name");
}
#[test]
fn test_version_diff_compute_detects_multiple_changes() {
let from_props = PropertyMapBuilder::new()
.insert("name", "Alice")
.insert("age", 30i64)
.insert("status", "active")
.build();
let to_props = PropertyMapBuilder::new()
.insert("name", "Alice") .insert("age", 31i64) .insert("city", "NYC") .build();
let diff = VersionDiff::compute(
&from_props,
&to_props,
test_version_id(1),
test_version_id(2),
);
assert_eq!(diff.added.len(), 1);
assert!(diff.added.contains_key("city"));
assert_eq!(diff.removed.len(), 1);
assert!(diff.removed.contains_key("status"));
assert_eq!(diff.modified.len(), 1);
let key_str = GLOBAL_INTERNER
.resolve_with(diff.modified[0].0, |s| s.to_string())
.unwrap();
assert_eq!(key_str, "age");
}
#[test]
fn test_version_diff_has_changes() {
let from_props = PropertyMapBuilder::new().insert("name", "Alice").build();
let to_props = PropertyMapBuilder::new().insert("name", "Bob").build();
let diff = VersionDiff::compute(
&from_props,
&to_props,
test_version_id(1),
test_version_id(2),
);
assert!(diff.has_changes());
assert_eq!(diff.change_count(), 1);
}
#[test]
fn test_version_diff_no_changes() {
let from_props = PropertyMapBuilder::new()
.insert("name", "Alice")
.insert("age", 30i64)
.build();
let to_props = PropertyMapBuilder::new()
.insert("name", "Alice")
.insert("age", 30i64)
.build();
let diff = VersionDiff::compute(
&from_props,
&to_props,
test_version_id(1),
test_version_id(2),
);
assert!(!diff.has_changes());
assert_eq!(diff.change_count(), 0);
}
#[test]
fn test_entity_history_version_count() {
let test_cases = vec![
(vec![], 0),
(vec![create_test_version_info(1, 1000)], 1),
(
vec![
create_test_version_info(1, 1000),
create_test_version_info(2, 2000),
],
2,
),
];
for (versions, expected_count) in test_cases {
let history = EntityHistory { versions };
assert_eq!(history.version_count(), expected_count);
}
}
#[test]
fn test_entity_history_current_version() {
let empty_history = EntityHistory { versions: vec![] };
assert!(empty_history.current_version().is_none());
let v1 = create_test_version_info(1, 1000);
let v2 = create_test_version_info(2, 2000);
let history = EntityHistory {
versions: vec![v1.clone(), v2.clone()],
};
let current = history.current_version().unwrap();
assert_eq!(current.version_number, 2);
assert_eq!(current.version_id, test_version_id(2));
}
#[test]
fn test_entity_history_first_version() {
let empty_history = EntityHistory { versions: vec![] };
assert!(empty_history.first_version().is_none());
let v1 = create_test_version_info(1, 1000);
let v2 = create_test_version_info(2, 2000);
let history = EntityHistory {
versions: vec![v1.clone(), v2.clone()],
};
let first = history.first_version().unwrap();
assert_eq!(first.version_number, 1);
assert_eq!(first.version_id, test_version_id(1));
}
#[test]
fn test_entity_history_empty() {
let history = EntityHistory { versions: vec![] };
assert_eq!(history.version_count(), 0);
assert!(history.current_version().is_none());
assert!(history.first_version().is_none());
}
#[test]
fn test_version_summary_changes_and_count() {
let test_cases = vec![
(0, 0, 0, false, 0),
(1, 0, 0, true, 1),
(0, 1, 0, true, 1),
(0, 0, 1, true, 1),
(1, 1, 0, true, 2),
(1, 0, 1, true, 2),
(0, 1, 1, true, 2),
(1, 1, 1, true, 3),
(2, 1, 3, true, 6),
(0, 0, 5, true, 5),
(5, 0, 0, true, 5),
(0, 5, 0, true, 5),
];
for (added, removed, modified, expected_has_changes, expected_change_count) in test_cases {
let summary = VersionSummary {
version_id: test_version_id(1),
version_number: 1,
valid_from: test_timestamp(1000),
transaction_time: test_timestamp(2000),
properties_added: added,
properties_removed: removed,
properties_modified: modified,
};
assert_eq!(
summary.has_changes(),
expected_has_changes,
"has_changes mismatch for added={}, removed={}, modified={}",
added,
removed,
modified
);
assert_eq!(
summary.change_count(),
expected_change_count,
"change_count mismatch for added={}, removed={}, modified={}",
added,
removed,
modified
);
}
}
#[test]
fn test_version_diff_has_changes_added_only() {
let from_props = PropertyMapBuilder::new().build();
let to_props = PropertyMapBuilder::new().insert("name", "Alice").build();
let diff = VersionDiff::compute(
&from_props,
&to_props,
test_version_id(10),
test_version_id(11),
);
assert_eq!(diff.added.len(), 1);
assert!(diff.removed.is_empty());
assert!(diff.modified.is_empty());
assert!(diff.has_changes());
}
#[test]
fn test_version_diff_has_changes_removed_only() {
let from_props = PropertyMapBuilder::new().insert("status", "active").build();
let to_props = PropertyMapBuilder::new().build();
let diff = VersionDiff::compute(
&from_props,
&to_props,
test_version_id(12),
test_version_id(13),
);
assert!(diff.added.is_empty());
assert_eq!(diff.removed.len(), 1);
assert!(diff.modified.is_empty());
assert!(diff.has_changes());
}
#[test]
fn test_version_diff_change_count_and_has_changes() {
let test_cases = vec![
(
PropertyMapBuilder::new().insert("name", "Alice").build(),
PropertyMapBuilder::new().insert("name", "Alice").build(),
0,
0,
0,
),
(
PropertyMapBuilder::new().build(),
PropertyMapBuilder::new().insert("name", "Alice").build(),
1,
0,
0,
),
(
PropertyMapBuilder::new().insert("name", "Alice").build(),
PropertyMapBuilder::new().build(),
0,
1,
0,
),
(
PropertyMapBuilder::new().insert("name", "Alice").build(),
PropertyMapBuilder::new().insert("name", "Bob").build(),
0,
0,
1,
),
(
PropertyMapBuilder::new()
.insert("name", "Alice")
.insert("age", 30i64)
.insert("status", "active")
.build(),
PropertyMapBuilder::new()
.insert("name", "Bob")
.insert("city", "NYC")
.build(),
1,
2,
1,
),
];
for (from_props, to_props, exp_added, exp_removed, exp_modified) in test_cases {
let diff = VersionDiff::compute(
&from_props,
&to_props,
test_version_id(1),
test_version_id(2),
);
assert_eq!(diff.added.len(), exp_added);
assert_eq!(diff.removed.len(), exp_removed);
assert_eq!(diff.modified.len(), exp_modified);
let expected_count = exp_added + exp_removed + exp_modified;
let expected_has_changes = expected_count > 0;
assert_eq!(
diff.has_changes(),
expected_has_changes,
"has_changes mismatch for {:?}",
diff
);
assert_eq!(
diff.change_count(),
expected_count,
"change_count mismatch for {:?}",
diff
);
}
}
#[test]
fn test_version_summary_has_changes_removed_only() {
let summary = VersionSummary {
version_id: test_version_id(3),
version_number: 3,
valid_from: test_timestamp(3000),
transaction_time: test_timestamp(4000),
properties_added: 0,
properties_removed: 1,
properties_modified: 0,
};
assert!(summary.has_changes());
assert_eq!(summary.change_count(), 1);
}
#[test]
fn test_version_summary_has_changes_modified_only() {
let summary = VersionSummary {
version_id: test_version_id(4),
version_number: 4,
valid_from: test_timestamp(4000),
transaction_time: test_timestamp(5000),
properties_added: 0,
properties_removed: 0,
properties_modified: 1,
};
assert!(summary.has_changes());
assert_eq!(summary.change_count(), 1);
}
fn create_test_version_info(version_num: u64, wallclock: i64) -> VersionInfo {
let timestamp = test_timestamp(wallclock);
VersionInfo {
version_number: version_num,
version_id: test_version_id(version_num),
temporal: BiTemporalInterval::with_valid_time(timestamp, timestamp),
properties: PropertyMapBuilder::new()
.insert("version", version_num as i64)
.build(),
label: "Test".to_string(),
}
}
#[test]
fn test_version_diff_nan_equality() {
let from_props = PropertyMapBuilder::new()
.insert("score", PropertyValue::Float(f64::NAN))
.build();
let to_props = PropertyMapBuilder::new()
.insert("score", PropertyValue::Float(f64::NAN))
.build();
let diff = VersionDiff::compute(
&from_props,
&to_props,
test_version_id(1),
test_version_id(2),
);
assert!(
diff.modified.is_empty(),
"VersionDiff should ignore NaN -> NaN 'changes' for Float"
);
assert!(!diff.has_changes(), "VersionDiff should not report changes");
}
#[test]
fn test_version_diff_vector_nan_equality() {
let vec_with_nan = vec![1.0, f32::NAN, 3.0];
let from_props = PropertyMapBuilder::new()
.insert("embedding", PropertyValue::vector(&vec_with_nan))
.build();
let to_props = PropertyMapBuilder::new()
.insert("embedding", PropertyValue::vector(&vec_with_nan))
.build();
let diff = VersionDiff::compute(
&from_props,
&to_props,
test_version_id(1),
test_version_id(2),
);
assert!(
diff.modified.is_empty(),
"VersionDiff should ignore NaN -> NaN 'changes' for Vector"
);
assert!(!diff.has_changes(), "VersionDiff should not report changes");
}
#[test]
fn test_version_diff_null_equality() {
let from_props = PropertyMapBuilder::new()
.insert("data", PropertyValue::Null)
.build();
let to_props = PropertyMapBuilder::new()
.insert("data", PropertyValue::Null)
.build();
let diff = VersionDiff::compute(
&from_props,
&to_props,
test_version_id(1),
test_version_id(2),
);
assert!(
diff.modified.is_empty(),
"VersionDiff should ignore Null -> Null"
);
assert!(!diff.has_changes());
}
#[test]
fn test_version_diff_compute_verifies_ids() {
let from_props = PropertyMapBuilder::new().build();
let to_props = PropertyMapBuilder::new().build();
let from_id = test_version_id(1);
let to_id = test_version_id(2);
let diff = VersionDiff::compute(&from_props, &to_props, from_id, to_id);
assert_eq!(diff.from_version, from_id, "from_version must match input");
assert_eq!(diff.to_version, to_id, "to_version must match input");
assert_ne!(from_id, to_id, "Test requires distinct IDs");
}
}