1use std::collections::HashSet;
7
8use crate::{DomainInfo, PredicateInfo, SymbolTable};
9
10#[derive(Clone, Debug, Default)]
14pub struct SchemaDiff {
15 pub domains_added: Vec<DomainInfo>,
17 pub domains_removed: Vec<DomainInfo>,
19 pub domains_modified: Vec<DomainModification>,
21 pub predicates_added: Vec<PredicateInfo>,
23 pub predicates_removed: Vec<PredicateInfo>,
25 pub predicates_modified: Vec<PredicateModification>,
27 pub variables_added: Vec<(String, String)>,
29 pub variables_removed: Vec<(String, String)>,
31 pub variables_modified: Vec<VariableModification>,
33}
34
35impl SchemaDiff {
36 pub fn has_changes(&self) -> bool {
38 !self.domains_added.is_empty()
39 || !self.domains_removed.is_empty()
40 || !self.domains_modified.is_empty()
41 || !self.predicates_added.is_empty()
42 || !self.predicates_removed.is_empty()
43 || !self.predicates_modified.is_empty()
44 || !self.variables_added.is_empty()
45 || !self.variables_removed.is_empty()
46 || !self.variables_modified.is_empty()
47 }
48
49 pub fn is_backward_compatible(&self) -> bool {
54 if !self.domains_removed.is_empty()
56 || !self.predicates_removed.is_empty()
57 || !self.variables_removed.is_empty()
58 {
59 return false;
60 }
61
62 for modification in &self.domains_modified {
64 if modification.new_cardinality < modification.old_cardinality {
66 return false;
67 }
68 }
69
70 for modification in &self.predicates_modified {
72 if modification.signature_changed {
74 return false;
75 }
76 }
77
78 if !self.variables_modified.is_empty() {
80 return false;
81 }
82
83 true
84 }
85
86 pub fn summary(&self) -> DiffSummary {
88 DiffSummary {
89 domains_added: self.domains_added.len(),
90 domains_removed: self.domains_removed.len(),
91 domains_modified: self.domains_modified.len(),
92 predicates_added: self.predicates_added.len(),
93 predicates_removed: self.predicates_removed.len(),
94 predicates_modified: self.predicates_modified.len(),
95 variables_added: self.variables_added.len(),
96 variables_removed: self.variables_removed.len(),
97 variables_modified: self.variables_modified.len(),
98 is_backward_compatible: self.is_backward_compatible(),
99 }
100 }
101
102 pub fn report(&self) -> String {
104 let mut output = String::new();
105
106 if !self.has_changes() {
107 output.push_str("No changes detected.\n");
108 return output;
109 }
110
111 let summary = self.summary();
112 output.push_str("Schema Diff Summary:\n");
113 output.push_str(&format!(
114 " Backward Compatible: {}\n\n",
115 summary.is_backward_compatible
116 ));
117
118 if !self.domains_added.is_empty() {
119 output.push_str(&format!("Domains Added ({}):\n", self.domains_added.len()));
120 for domain in &self.domains_added {
121 output.push_str(&format!(
122 " + {} (cardinality: {})\n",
123 domain.name, domain.cardinality
124 ));
125 }
126 output.push('\n');
127 }
128
129 if !self.domains_removed.is_empty() {
130 output.push_str(&format!(
131 "Domains Removed ({}):\n",
132 self.domains_removed.len()
133 ));
134 for domain in &self.domains_removed {
135 output.push_str(&format!(
136 " - {} (cardinality: {})\n",
137 domain.name, domain.cardinality
138 ));
139 }
140 output.push('\n');
141 }
142
143 if !self.domains_modified.is_empty() {
144 output.push_str(&format!(
145 "Domains Modified ({}):\n",
146 self.domains_modified.len()
147 ));
148 for modification in &self.domains_modified {
149 output.push_str(&format!(" ~ {}\n", modification.domain_name));
150 if modification.old_cardinality != modification.new_cardinality {
151 output.push_str(&format!(
152 " cardinality: {} -> {}\n",
153 modification.old_cardinality, modification.new_cardinality
154 ));
155 }
156 if modification.description_changed {
157 output.push_str(" description: changed\n");
158 }
159 }
160 output.push('\n');
161 }
162
163 if !self.predicates_added.is_empty() {
164 output.push_str(&format!(
165 "Predicates Added ({}):\n",
166 self.predicates_added.len()
167 ));
168 for pred in &self.predicates_added {
169 output.push_str(&format!(
170 " + {} (arity: {})\n",
171 pred.name,
172 pred.arg_domains.len()
173 ));
174 }
175 output.push('\n');
176 }
177
178 if !self.predicates_removed.is_empty() {
179 output.push_str(&format!(
180 "Predicates Removed ({}):\n",
181 self.predicates_removed.len()
182 ));
183 for pred in &self.predicates_removed {
184 output.push_str(&format!(
185 " - {} (arity: {})\n",
186 pred.name,
187 pred.arg_domains.len()
188 ));
189 }
190 output.push('\n');
191 }
192
193 if !self.predicates_modified.is_empty() {
194 output.push_str(&format!(
195 "Predicates Modified ({}):\n",
196 self.predicates_modified.len()
197 ));
198 for modification in &self.predicates_modified {
199 output.push_str(&format!(" ~ {}\n", modification.predicate_name));
200 if modification.signature_changed {
201 output.push_str(&format!(
202 " signature: {:?} -> {:?}\n",
203 modification.old_signature, modification.new_signature
204 ));
205 }
206 }
207 output.push('\n');
208 }
209
210 output
211 }
212}
213
214#[derive(Clone, Debug)]
216pub struct DomainModification {
217 pub domain_name: String,
219 pub old_cardinality: usize,
221 pub new_cardinality: usize,
223 pub description_changed: bool,
225 pub metadata_changed: bool,
227}
228
229#[derive(Clone, Debug)]
231pub struct PredicateModification {
232 pub predicate_name: String,
234 pub signature_changed: bool,
236 pub old_signature: Vec<String>,
238 pub new_signature: Vec<String>,
240 pub description_changed: bool,
242}
243
244#[derive(Clone, Debug)]
246pub struct VariableModification {
247 pub variable_name: String,
249 pub old_domain: String,
251 pub new_domain: String,
253}
254
255#[derive(Clone, Debug)]
257pub struct DiffSummary {
258 pub domains_added: usize,
260 pub domains_removed: usize,
262 pub domains_modified: usize,
264 pub predicates_added: usize,
266 pub predicates_removed: usize,
268 pub predicates_modified: usize,
270 pub variables_added: usize,
272 pub variables_removed: usize,
274 pub variables_modified: usize,
276 pub is_backward_compatible: bool,
278}
279
280impl DiffSummary {
281 pub fn total_changes(&self) -> usize {
283 self.domains_added
284 + self.domains_removed
285 + self.domains_modified
286 + self.predicates_added
287 + self.predicates_removed
288 + self.predicates_modified
289 + self.variables_added
290 + self.variables_removed
291 + self.variables_modified
292 }
293}
294
295pub fn compute_diff(old: &SymbolTable, new: &SymbolTable) -> SchemaDiff {
314 let mut diff = SchemaDiff::default();
315
316 let old_domain_names: HashSet<_> = old.domains.keys().collect();
318 let new_domain_names: HashSet<_> = new.domains.keys().collect();
319
320 for name in new_domain_names.difference(&old_domain_names) {
321 diff.domains_added.push(new.domains[*name].clone());
322 }
323
324 for name in old_domain_names.difference(&new_domain_names) {
325 diff.domains_removed.push(old.domains[*name].clone());
326 }
327
328 for name in old_domain_names.intersection(&new_domain_names) {
329 let old_domain = &old.domains[*name];
330 let new_domain = &new.domains[*name];
331
332 if old_domain.cardinality != new_domain.cardinality
333 || old_domain.description != new_domain.description
334 || old_domain.metadata != new_domain.metadata
335 {
336 diff.domains_modified.push(DomainModification {
337 domain_name: (*name).clone(),
338 old_cardinality: old_domain.cardinality,
339 new_cardinality: new_domain.cardinality,
340 description_changed: old_domain.description != new_domain.description,
341 metadata_changed: old_domain.metadata != new_domain.metadata,
342 });
343 }
344 }
345
346 let old_pred_names: HashSet<_> = old.predicates.keys().collect();
348 let new_pred_names: HashSet<_> = new.predicates.keys().collect();
349
350 for name in new_pred_names.difference(&old_pred_names) {
351 diff.predicates_added.push(new.predicates[*name].clone());
352 }
353
354 for name in old_pred_names.difference(&new_pred_names) {
355 diff.predicates_removed.push(old.predicates[*name].clone());
356 }
357
358 for name in old_pred_names.intersection(&new_pred_names) {
359 let old_pred = &old.predicates[*name];
360 let new_pred = &new.predicates[*name];
361
362 let signature_changed = old_pred.arg_domains != new_pred.arg_domains;
363 let description_changed = old_pred.description != new_pred.description;
364
365 if signature_changed || description_changed {
366 diff.predicates_modified.push(PredicateModification {
367 predicate_name: (*name).clone(),
368 signature_changed,
369 old_signature: old_pred.arg_domains.clone(),
370 new_signature: new_pred.arg_domains.clone(),
371 description_changed,
372 });
373 }
374 }
375
376 let old_var_names: HashSet<_> = old.variables.keys().collect();
378 let new_var_names: HashSet<_> = new.variables.keys().collect();
379
380 for name in new_var_names.difference(&old_var_names) {
381 diff.variables_added
382 .push(((*name).clone(), new.variables[*name].clone()));
383 }
384
385 for name in old_var_names.difference(&new_var_names) {
386 diff.variables_removed
387 .push(((*name).clone(), old.variables[*name].clone()));
388 }
389
390 for name in old_var_names.intersection(&new_var_names) {
391 let old_domain = &old.variables[*name];
392 let new_domain = &new.variables[*name];
393
394 if old_domain != new_domain {
395 diff.variables_modified.push(VariableModification {
396 variable_name: (*name).clone(),
397 old_domain: old_domain.clone(),
398 new_domain: new_domain.clone(),
399 });
400 }
401 }
402
403 diff
404}
405
406pub fn merge_tables(base: &SymbolTable, update: &SymbolTable) -> SymbolTable {
410 let mut merged = base.clone();
411
412 for (name, domain) in &update.domains {
414 merged.domains.insert(name.clone(), domain.clone());
415 }
416
417 for (name, predicate) in &update.predicates {
419 merged.predicates.insert(name.clone(), predicate.clone());
420 }
421
422 for (name, domain) in &update.variables {
424 merged.variables.insert(name.clone(), domain.clone());
425 }
426
427 merged
428}
429
430#[derive(Clone, Debug, PartialEq, Eq)]
432pub enum CompatibilityLevel {
433 Identical,
435 BackwardCompatible,
437 ForwardCompatible,
439 Breaking,
441}
442
443pub fn check_compatibility(old: &SymbolTable, new: &SymbolTable) -> CompatibilityLevel {
445 let diff = compute_diff(old, new);
446
447 if !diff.has_changes() {
448 return CompatibilityLevel::Identical;
449 }
450
451 if diff.is_backward_compatible() {
452 return CompatibilityLevel::BackwardCompatible;
453 }
454
455 if diff.domains_added.is_empty()
457 && diff.predicates_added.is_empty()
458 && diff.variables_added.is_empty()
459 && diff.domains_modified.is_empty()
460 && diff.predicates_modified.is_empty()
461 && diff.variables_modified.is_empty()
462 {
463 return CompatibilityLevel::ForwardCompatible;
464 }
465
466 CompatibilityLevel::Breaking
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472
473 #[test]
474 fn test_identical_schemas() {
475 let mut table = SymbolTable::new();
476 table.add_domain(DomainInfo::new("Person", 100)).unwrap();
477
478 let diff = compute_diff(&table, &table);
479 assert!(!diff.has_changes());
480 assert!(diff.is_backward_compatible());
481 assert_eq!(
482 check_compatibility(&table, &table),
483 CompatibilityLevel::Identical
484 );
485 }
486
487 #[test]
488 fn test_domain_addition() {
489 let mut old_table = SymbolTable::new();
490 old_table
491 .add_domain(DomainInfo::new("Person", 100))
492 .unwrap();
493
494 let mut new_table = old_table.clone();
495 new_table
496 .add_domain(DomainInfo::new("Location", 50))
497 .unwrap();
498
499 let diff = compute_diff(&old_table, &new_table);
500 assert_eq!(diff.domains_added.len(), 1);
501 assert_eq!(diff.domains_added[0].name, "Location");
502 assert!(diff.is_backward_compatible());
503 assert_eq!(
504 check_compatibility(&old_table, &new_table),
505 CompatibilityLevel::BackwardCompatible
506 );
507 }
508
509 #[test]
510 fn test_domain_removal() {
511 let mut old_table = SymbolTable::new();
512 old_table
513 .add_domain(DomainInfo::new("Person", 100))
514 .unwrap();
515 old_table
516 .add_domain(DomainInfo::new("Location", 50))
517 .unwrap();
518
519 let mut new_table = SymbolTable::new();
520 new_table
521 .add_domain(DomainInfo::new("Person", 100))
522 .unwrap();
523
524 let diff = compute_diff(&old_table, &new_table);
525 assert_eq!(diff.domains_removed.len(), 1);
526 assert_eq!(diff.domains_removed[0].name, "Location");
527 assert!(!diff.is_backward_compatible());
528 assert_eq!(
529 check_compatibility(&old_table, &new_table),
530 CompatibilityLevel::ForwardCompatible
531 );
532 }
533
534 #[test]
535 fn test_domain_modification() {
536 let mut old_table = SymbolTable::new();
537 old_table
538 .add_domain(DomainInfo::new("Person", 100))
539 .unwrap();
540
541 let mut new_table = SymbolTable::new();
542 new_table
543 .add_domain(DomainInfo::new("Person", 200))
544 .unwrap();
545
546 let diff = compute_diff(&old_table, &new_table);
547 assert_eq!(diff.domains_modified.len(), 1);
548 assert_eq!(diff.domains_modified[0].old_cardinality, 100);
549 assert_eq!(diff.domains_modified[0].new_cardinality, 200);
550 assert!(diff.is_backward_compatible()); }
552
553 #[test]
554 fn test_cardinality_reduction_breaks_compatibility() {
555 let mut old_table = SymbolTable::new();
556 old_table
557 .add_domain(DomainInfo::new("Person", 200))
558 .unwrap();
559
560 let mut new_table = SymbolTable::new();
561 new_table
562 .add_domain(DomainInfo::new("Person", 100))
563 .unwrap();
564
565 let diff = compute_diff(&old_table, &new_table);
566 assert!(!diff.is_backward_compatible());
567 assert_eq!(
568 check_compatibility(&old_table, &new_table),
569 CompatibilityLevel::Breaking
570 );
571 }
572
573 #[test]
574 fn test_predicate_addition() {
575 let mut old_table = SymbolTable::new();
576 old_table
577 .add_domain(DomainInfo::new("Person", 100))
578 .unwrap();
579
580 let mut new_table = old_table.clone();
581 new_table
582 .add_predicate(PredicateInfo::new("knows", vec!["Person".to_string()]))
583 .unwrap();
584
585 let diff = compute_diff(&old_table, &new_table);
586 assert_eq!(diff.predicates_added.len(), 1);
587 assert!(diff.is_backward_compatible());
588 }
589
590 #[test]
591 fn test_predicate_signature_change() {
592 let mut old_table = SymbolTable::new();
593 old_table
594 .add_domain(DomainInfo::new("Person", 100))
595 .unwrap();
596 old_table
597 .add_predicate(PredicateInfo::new("knows", vec!["Person".to_string()]))
598 .unwrap();
599
600 let mut new_table = SymbolTable::new();
601 new_table
602 .add_domain(DomainInfo::new("Person", 100))
603 .unwrap();
604 new_table
605 .add_predicate(PredicateInfo::new(
606 "knows",
607 vec!["Person".to_string(), "Person".to_string()],
608 ))
609 .unwrap();
610
611 let diff = compute_diff(&old_table, &new_table);
612 assert_eq!(diff.predicates_modified.len(), 1);
613 assert!(diff.predicates_modified[0].signature_changed);
614 assert!(!diff.is_backward_compatible());
615 }
616
617 #[test]
618 fn test_merge_tables() {
619 let mut base = SymbolTable::new();
620 base.add_domain(DomainInfo::new("Person", 100)).unwrap();
621
622 let mut update = SymbolTable::new();
623 update.add_domain(DomainInfo::new("Person", 200)).unwrap();
624 update.add_domain(DomainInfo::new("Location", 50)).unwrap();
625
626 let merged = merge_tables(&base, &update);
627 assert_eq!(merged.domains.len(), 2);
628 assert_eq!(merged.get_domain("Person").unwrap().cardinality, 200);
629 assert!(merged.get_domain("Location").is_some());
630 }
631
632 #[test]
633 fn test_diff_report() {
634 let mut old_table = SymbolTable::new();
635 old_table
636 .add_domain(DomainInfo::new("Person", 100))
637 .unwrap();
638
639 let mut new_table = old_table.clone();
640 new_table
641 .add_domain(DomainInfo::new("Location", 50))
642 .unwrap();
643
644 let diff = compute_diff(&old_table, &new_table);
645 let report = diff.report();
646 assert!(report.contains("Domains Added"));
647 assert!(report.contains("Location"));
648 }
649
650 #[test]
651 fn test_summary_total_changes() {
652 let mut old_table = SymbolTable::new();
653 old_table
654 .add_domain(DomainInfo::new("Person", 100))
655 .unwrap();
656
657 let mut new_table = old_table.clone();
658 new_table
659 .add_domain(DomainInfo::new("Location", 50))
660 .unwrap();
661 new_table.add_domain(DomainInfo::new("Event", 30)).unwrap();
662
663 let diff = compute_diff(&old_table, &new_table);
664 let summary = diff.summary();
665 assert_eq!(summary.total_changes(), 2);
666 }
667}