use std::collections::HashSet;
use crate::{DomainInfo, PredicateInfo, SymbolTable};
#[derive(Clone, Debug, Default)]
pub struct SchemaDiff {
pub domains_added: Vec<DomainInfo>,
pub domains_removed: Vec<DomainInfo>,
pub domains_modified: Vec<DomainModification>,
pub predicates_added: Vec<PredicateInfo>,
pub predicates_removed: Vec<PredicateInfo>,
pub predicates_modified: Vec<PredicateModification>,
pub variables_added: Vec<(String, String)>,
pub variables_removed: Vec<(String, String)>,
pub variables_modified: Vec<VariableModification>,
}
impl SchemaDiff {
pub fn has_changes(&self) -> bool {
!self.domains_added.is_empty()
|| !self.domains_removed.is_empty()
|| !self.domains_modified.is_empty()
|| !self.predicates_added.is_empty()
|| !self.predicates_removed.is_empty()
|| !self.predicates_modified.is_empty()
|| !self.variables_added.is_empty()
|| !self.variables_removed.is_empty()
|| !self.variables_modified.is_empty()
}
pub fn is_backward_compatible(&self) -> bool {
if !self.domains_removed.is_empty()
|| !self.predicates_removed.is_empty()
|| !self.variables_removed.is_empty()
{
return false;
}
for modification in &self.domains_modified {
if modification.new_cardinality < modification.old_cardinality {
return false;
}
}
for modification in &self.predicates_modified {
if modification.signature_changed {
return false;
}
}
if !self.variables_modified.is_empty() {
return false;
}
true
}
pub fn summary(&self) -> DiffSummary {
DiffSummary {
domains_added: self.domains_added.len(),
domains_removed: self.domains_removed.len(),
domains_modified: self.domains_modified.len(),
predicates_added: self.predicates_added.len(),
predicates_removed: self.predicates_removed.len(),
predicates_modified: self.predicates_modified.len(),
variables_added: self.variables_added.len(),
variables_removed: self.variables_removed.len(),
variables_modified: self.variables_modified.len(),
is_backward_compatible: self.is_backward_compatible(),
}
}
pub fn report(&self) -> String {
let mut output = String::new();
if !self.has_changes() {
output.push_str("No changes detected.\n");
return output;
}
let summary = self.summary();
output.push_str("Schema Diff Summary:\n");
output.push_str(&format!(
" Backward Compatible: {}\n\n",
summary.is_backward_compatible
));
if !self.domains_added.is_empty() {
output.push_str(&format!("Domains Added ({}):\n", self.domains_added.len()));
for domain in &self.domains_added {
output.push_str(&format!(
" + {} (cardinality: {})\n",
domain.name, domain.cardinality
));
}
output.push('\n');
}
if !self.domains_removed.is_empty() {
output.push_str(&format!(
"Domains Removed ({}):\n",
self.domains_removed.len()
));
for domain in &self.domains_removed {
output.push_str(&format!(
" - {} (cardinality: {})\n",
domain.name, domain.cardinality
));
}
output.push('\n');
}
if !self.domains_modified.is_empty() {
output.push_str(&format!(
"Domains Modified ({}):\n",
self.domains_modified.len()
));
for modification in &self.domains_modified {
output.push_str(&format!(" ~ {}\n", modification.domain_name));
if modification.old_cardinality != modification.new_cardinality {
output.push_str(&format!(
" cardinality: {} -> {}\n",
modification.old_cardinality, modification.new_cardinality
));
}
if modification.description_changed {
output.push_str(" description: changed\n");
}
}
output.push('\n');
}
if !self.predicates_added.is_empty() {
output.push_str(&format!(
"Predicates Added ({}):\n",
self.predicates_added.len()
));
for pred in &self.predicates_added {
output.push_str(&format!(
" + {} (arity: {})\n",
pred.name,
pred.arg_domains.len()
));
}
output.push('\n');
}
if !self.predicates_removed.is_empty() {
output.push_str(&format!(
"Predicates Removed ({}):\n",
self.predicates_removed.len()
));
for pred in &self.predicates_removed {
output.push_str(&format!(
" - {} (arity: {})\n",
pred.name,
pred.arg_domains.len()
));
}
output.push('\n');
}
if !self.predicates_modified.is_empty() {
output.push_str(&format!(
"Predicates Modified ({}):\n",
self.predicates_modified.len()
));
for modification in &self.predicates_modified {
output.push_str(&format!(" ~ {}\n", modification.predicate_name));
if modification.signature_changed {
output.push_str(&format!(
" signature: {:?} -> {:?}\n",
modification.old_signature, modification.new_signature
));
}
}
output.push('\n');
}
output
}
}
#[derive(Clone, Debug)]
pub struct DomainModification {
pub domain_name: String,
pub old_cardinality: usize,
pub new_cardinality: usize,
pub description_changed: bool,
pub metadata_changed: bool,
}
#[derive(Clone, Debug)]
pub struct PredicateModification {
pub predicate_name: String,
pub signature_changed: bool,
pub old_signature: Vec<String>,
pub new_signature: Vec<String>,
pub description_changed: bool,
}
#[derive(Clone, Debug)]
pub struct VariableModification {
pub variable_name: String,
pub old_domain: String,
pub new_domain: String,
}
#[derive(Clone, Debug)]
pub struct DiffSummary {
pub domains_added: usize,
pub domains_removed: usize,
pub domains_modified: usize,
pub predicates_added: usize,
pub predicates_removed: usize,
pub predicates_modified: usize,
pub variables_added: usize,
pub variables_removed: usize,
pub variables_modified: usize,
pub is_backward_compatible: bool,
}
impl DiffSummary {
pub fn total_changes(&self) -> usize {
self.domains_added
+ self.domains_removed
+ self.domains_modified
+ self.predicates_added
+ self.predicates_removed
+ self.predicates_modified
+ self.variables_added
+ self.variables_removed
+ self.variables_modified
}
}
pub fn compute_diff(old: &SymbolTable, new: &SymbolTable) -> SchemaDiff {
let mut diff = SchemaDiff::default();
let old_domain_names: HashSet<_> = old.domains.keys().collect();
let new_domain_names: HashSet<_> = new.domains.keys().collect();
for name in new_domain_names.difference(&old_domain_names) {
diff.domains_added.push(new.domains[*name].clone());
}
for name in old_domain_names.difference(&new_domain_names) {
diff.domains_removed.push(old.domains[*name].clone());
}
for name in old_domain_names.intersection(&new_domain_names) {
let old_domain = &old.domains[*name];
let new_domain = &new.domains[*name];
if old_domain.cardinality != new_domain.cardinality
|| old_domain.description != new_domain.description
|| old_domain.metadata != new_domain.metadata
{
diff.domains_modified.push(DomainModification {
domain_name: (*name).clone(),
old_cardinality: old_domain.cardinality,
new_cardinality: new_domain.cardinality,
description_changed: old_domain.description != new_domain.description,
metadata_changed: old_domain.metadata != new_domain.metadata,
});
}
}
let old_pred_names: HashSet<_> = old.predicates.keys().collect();
let new_pred_names: HashSet<_> = new.predicates.keys().collect();
for name in new_pred_names.difference(&old_pred_names) {
diff.predicates_added.push(new.predicates[*name].clone());
}
for name in old_pred_names.difference(&new_pred_names) {
diff.predicates_removed.push(old.predicates[*name].clone());
}
for name in old_pred_names.intersection(&new_pred_names) {
let old_pred = &old.predicates[*name];
let new_pred = &new.predicates[*name];
let signature_changed = old_pred.arg_domains != new_pred.arg_domains;
let description_changed = old_pred.description != new_pred.description;
if signature_changed || description_changed {
diff.predicates_modified.push(PredicateModification {
predicate_name: (*name).clone(),
signature_changed,
old_signature: old_pred.arg_domains.clone(),
new_signature: new_pred.arg_domains.clone(),
description_changed,
});
}
}
let old_var_names: HashSet<_> = old.variables.keys().collect();
let new_var_names: HashSet<_> = new.variables.keys().collect();
for name in new_var_names.difference(&old_var_names) {
diff.variables_added
.push(((*name).clone(), new.variables[*name].clone()));
}
for name in old_var_names.difference(&new_var_names) {
diff.variables_removed
.push(((*name).clone(), old.variables[*name].clone()));
}
for name in old_var_names.intersection(&new_var_names) {
let old_domain = &old.variables[*name];
let new_domain = &new.variables[*name];
if old_domain != new_domain {
diff.variables_modified.push(VariableModification {
variable_name: (*name).clone(),
old_domain: old_domain.clone(),
new_domain: new_domain.clone(),
});
}
}
diff
}
pub fn merge_tables(base: &SymbolTable, update: &SymbolTable) -> SymbolTable {
let mut merged = base.clone();
for (name, domain) in &update.domains {
merged.domains.insert(name.clone(), domain.clone());
}
for (name, predicate) in &update.predicates {
merged.predicates.insert(name.clone(), predicate.clone());
}
for (name, domain) in &update.variables {
merged.variables.insert(name.clone(), domain.clone());
}
merged
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CompatibilityLevel {
Identical,
BackwardCompatible,
ForwardCompatible,
Breaking,
}
pub fn check_compatibility(old: &SymbolTable, new: &SymbolTable) -> CompatibilityLevel {
let diff = compute_diff(old, new);
if !diff.has_changes() {
return CompatibilityLevel::Identical;
}
if diff.is_backward_compatible() {
return CompatibilityLevel::BackwardCompatible;
}
if diff.domains_added.is_empty()
&& diff.predicates_added.is_empty()
&& diff.variables_added.is_empty()
&& diff.domains_modified.is_empty()
&& diff.predicates_modified.is_empty()
&& diff.variables_modified.is_empty()
{
return CompatibilityLevel::ForwardCompatible;
}
CompatibilityLevel::Breaking
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_identical_schemas() {
let mut table = SymbolTable::new();
table
.add_domain(DomainInfo::new("Person", 100))
.expect("unwrap");
let diff = compute_diff(&table, &table);
assert!(!diff.has_changes());
assert!(diff.is_backward_compatible());
assert_eq!(
check_compatibility(&table, &table),
CompatibilityLevel::Identical
);
}
#[test]
fn test_domain_addition() {
let mut old_table = SymbolTable::new();
old_table
.add_domain(DomainInfo::new("Person", 100))
.expect("unwrap");
let mut new_table = old_table.clone();
new_table
.add_domain(DomainInfo::new("Location", 50))
.expect("unwrap");
let diff = compute_diff(&old_table, &new_table);
assert_eq!(diff.domains_added.len(), 1);
assert_eq!(diff.domains_added[0].name, "Location");
assert!(diff.is_backward_compatible());
assert_eq!(
check_compatibility(&old_table, &new_table),
CompatibilityLevel::BackwardCompatible
);
}
#[test]
fn test_domain_removal() {
let mut old_table = SymbolTable::new();
old_table
.add_domain(DomainInfo::new("Person", 100))
.expect("unwrap");
old_table
.add_domain(DomainInfo::new("Location", 50))
.expect("unwrap");
let mut new_table = SymbolTable::new();
new_table
.add_domain(DomainInfo::new("Person", 100))
.expect("unwrap");
let diff = compute_diff(&old_table, &new_table);
assert_eq!(diff.domains_removed.len(), 1);
assert_eq!(diff.domains_removed[0].name, "Location");
assert!(!diff.is_backward_compatible());
assert_eq!(
check_compatibility(&old_table, &new_table),
CompatibilityLevel::ForwardCompatible
);
}
#[test]
fn test_domain_modification() {
let mut old_table = SymbolTable::new();
old_table
.add_domain(DomainInfo::new("Person", 100))
.expect("unwrap");
let mut new_table = SymbolTable::new();
new_table
.add_domain(DomainInfo::new("Person", 200))
.expect("unwrap");
let diff = compute_diff(&old_table, &new_table);
assert_eq!(diff.domains_modified.len(), 1);
assert_eq!(diff.domains_modified[0].old_cardinality, 100);
assert_eq!(diff.domains_modified[0].new_cardinality, 200);
assert!(diff.is_backward_compatible()); }
#[test]
fn test_cardinality_reduction_breaks_compatibility() {
let mut old_table = SymbolTable::new();
old_table
.add_domain(DomainInfo::new("Person", 200))
.expect("unwrap");
let mut new_table = SymbolTable::new();
new_table
.add_domain(DomainInfo::new("Person", 100))
.expect("unwrap");
let diff = compute_diff(&old_table, &new_table);
assert!(!diff.is_backward_compatible());
assert_eq!(
check_compatibility(&old_table, &new_table),
CompatibilityLevel::Breaking
);
}
#[test]
fn test_predicate_addition() {
let mut old_table = SymbolTable::new();
old_table
.add_domain(DomainInfo::new("Person", 100))
.expect("unwrap");
let mut new_table = old_table.clone();
new_table
.add_predicate(PredicateInfo::new("knows", vec!["Person".to_string()]))
.expect("unwrap");
let diff = compute_diff(&old_table, &new_table);
assert_eq!(diff.predicates_added.len(), 1);
assert!(diff.is_backward_compatible());
}
#[test]
fn test_predicate_signature_change() {
let mut old_table = SymbolTable::new();
old_table
.add_domain(DomainInfo::new("Person", 100))
.expect("unwrap");
old_table
.add_predicate(PredicateInfo::new("knows", vec!["Person".to_string()]))
.expect("unwrap");
let mut new_table = SymbolTable::new();
new_table
.add_domain(DomainInfo::new("Person", 100))
.expect("unwrap");
new_table
.add_predicate(PredicateInfo::new(
"knows",
vec!["Person".to_string(), "Person".to_string()],
))
.expect("unwrap");
let diff = compute_diff(&old_table, &new_table);
assert_eq!(diff.predicates_modified.len(), 1);
assert!(diff.predicates_modified[0].signature_changed);
assert!(!diff.is_backward_compatible());
}
#[test]
fn test_merge_tables() {
let mut base = SymbolTable::new();
base.add_domain(DomainInfo::new("Person", 100))
.expect("unwrap");
let mut update = SymbolTable::new();
update
.add_domain(DomainInfo::new("Person", 200))
.expect("unwrap");
update
.add_domain(DomainInfo::new("Location", 50))
.expect("unwrap");
let merged = merge_tables(&base, &update);
assert_eq!(merged.domains.len(), 2);
assert_eq!(
merged.get_domain("Person").expect("unwrap").cardinality,
200
);
assert!(merged.get_domain("Location").is_some());
}
#[test]
fn test_diff_report() {
let mut old_table = SymbolTable::new();
old_table
.add_domain(DomainInfo::new("Person", 100))
.expect("unwrap");
let mut new_table = old_table.clone();
new_table
.add_domain(DomainInfo::new("Location", 50))
.expect("unwrap");
let diff = compute_diff(&old_table, &new_table);
let report = diff.report();
assert!(report.contains("Domains Added"));
assert!(report.contains("Location"));
}
#[test]
fn test_summary_total_changes() {
let mut old_table = SymbolTable::new();
old_table
.add_domain(DomainInfo::new("Person", 100))
.expect("unwrap");
let mut new_table = old_table.clone();
new_table
.add_domain(DomainInfo::new("Location", 50))
.expect("unwrap");
new_table
.add_domain(DomainInfo::new("Event", 30))
.expect("unwrap");
let diff = compute_diff(&old_table, &new_table);
let summary = diff.summary();
assert_eq!(summary.total_changes(), 2);
}
}