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
477 .add_domain(DomainInfo::new("Person", 100))
478 .expect("unwrap");
479
480 let diff = compute_diff(&table, &table);
481 assert!(!diff.has_changes());
482 assert!(diff.is_backward_compatible());
483 assert_eq!(
484 check_compatibility(&table, &table),
485 CompatibilityLevel::Identical
486 );
487 }
488
489 #[test]
490 fn test_domain_addition() {
491 let mut old_table = SymbolTable::new();
492 old_table
493 .add_domain(DomainInfo::new("Person", 100))
494 .expect("unwrap");
495
496 let mut new_table = old_table.clone();
497 new_table
498 .add_domain(DomainInfo::new("Location", 50))
499 .expect("unwrap");
500
501 let diff = compute_diff(&old_table, &new_table);
502 assert_eq!(diff.domains_added.len(), 1);
503 assert_eq!(diff.domains_added[0].name, "Location");
504 assert!(diff.is_backward_compatible());
505 assert_eq!(
506 check_compatibility(&old_table, &new_table),
507 CompatibilityLevel::BackwardCompatible
508 );
509 }
510
511 #[test]
512 fn test_domain_removal() {
513 let mut old_table = SymbolTable::new();
514 old_table
515 .add_domain(DomainInfo::new("Person", 100))
516 .expect("unwrap");
517 old_table
518 .add_domain(DomainInfo::new("Location", 50))
519 .expect("unwrap");
520
521 let mut new_table = SymbolTable::new();
522 new_table
523 .add_domain(DomainInfo::new("Person", 100))
524 .expect("unwrap");
525
526 let diff = compute_diff(&old_table, &new_table);
527 assert_eq!(diff.domains_removed.len(), 1);
528 assert_eq!(diff.domains_removed[0].name, "Location");
529 assert!(!diff.is_backward_compatible());
530 assert_eq!(
531 check_compatibility(&old_table, &new_table),
532 CompatibilityLevel::ForwardCompatible
533 );
534 }
535
536 #[test]
537 fn test_domain_modification() {
538 let mut old_table = SymbolTable::new();
539 old_table
540 .add_domain(DomainInfo::new("Person", 100))
541 .expect("unwrap");
542
543 let mut new_table = SymbolTable::new();
544 new_table
545 .add_domain(DomainInfo::new("Person", 200))
546 .expect("unwrap");
547
548 let diff = compute_diff(&old_table, &new_table);
549 assert_eq!(diff.domains_modified.len(), 1);
550 assert_eq!(diff.domains_modified[0].old_cardinality, 100);
551 assert_eq!(diff.domains_modified[0].new_cardinality, 200);
552 assert!(diff.is_backward_compatible()); }
554
555 #[test]
556 fn test_cardinality_reduction_breaks_compatibility() {
557 let mut old_table = SymbolTable::new();
558 old_table
559 .add_domain(DomainInfo::new("Person", 200))
560 .expect("unwrap");
561
562 let mut new_table = SymbolTable::new();
563 new_table
564 .add_domain(DomainInfo::new("Person", 100))
565 .expect("unwrap");
566
567 let diff = compute_diff(&old_table, &new_table);
568 assert!(!diff.is_backward_compatible());
569 assert_eq!(
570 check_compatibility(&old_table, &new_table),
571 CompatibilityLevel::Breaking
572 );
573 }
574
575 #[test]
576 fn test_predicate_addition() {
577 let mut old_table = SymbolTable::new();
578 old_table
579 .add_domain(DomainInfo::new("Person", 100))
580 .expect("unwrap");
581
582 let mut new_table = old_table.clone();
583 new_table
584 .add_predicate(PredicateInfo::new("knows", vec!["Person".to_string()]))
585 .expect("unwrap");
586
587 let diff = compute_diff(&old_table, &new_table);
588 assert_eq!(diff.predicates_added.len(), 1);
589 assert!(diff.is_backward_compatible());
590 }
591
592 #[test]
593 fn test_predicate_signature_change() {
594 let mut old_table = SymbolTable::new();
595 old_table
596 .add_domain(DomainInfo::new("Person", 100))
597 .expect("unwrap");
598 old_table
599 .add_predicate(PredicateInfo::new("knows", vec!["Person".to_string()]))
600 .expect("unwrap");
601
602 let mut new_table = SymbolTable::new();
603 new_table
604 .add_domain(DomainInfo::new("Person", 100))
605 .expect("unwrap");
606 new_table
607 .add_predicate(PredicateInfo::new(
608 "knows",
609 vec!["Person".to_string(), "Person".to_string()],
610 ))
611 .expect("unwrap");
612
613 let diff = compute_diff(&old_table, &new_table);
614 assert_eq!(diff.predicates_modified.len(), 1);
615 assert!(diff.predicates_modified[0].signature_changed);
616 assert!(!diff.is_backward_compatible());
617 }
618
619 #[test]
620 fn test_merge_tables() {
621 let mut base = SymbolTable::new();
622 base.add_domain(DomainInfo::new("Person", 100))
623 .expect("unwrap");
624
625 let mut update = SymbolTable::new();
626 update
627 .add_domain(DomainInfo::new("Person", 200))
628 .expect("unwrap");
629 update
630 .add_domain(DomainInfo::new("Location", 50))
631 .expect("unwrap");
632
633 let merged = merge_tables(&base, &update);
634 assert_eq!(merged.domains.len(), 2);
635 assert_eq!(
636 merged.get_domain("Person").expect("unwrap").cardinality,
637 200
638 );
639 assert!(merged.get_domain("Location").is_some());
640 }
641
642 #[test]
643 fn test_diff_report() {
644 let mut old_table = SymbolTable::new();
645 old_table
646 .add_domain(DomainInfo::new("Person", 100))
647 .expect("unwrap");
648
649 let mut new_table = old_table.clone();
650 new_table
651 .add_domain(DomainInfo::new("Location", 50))
652 .expect("unwrap");
653
654 let diff = compute_diff(&old_table, &new_table);
655 let report = diff.report();
656 assert!(report.contains("Domains Added"));
657 assert!(report.contains("Location"));
658 }
659
660 #[test]
661 fn test_summary_total_changes() {
662 let mut old_table = SymbolTable::new();
663 old_table
664 .add_domain(DomainInfo::new("Person", 100))
665 .expect("unwrap");
666
667 let mut new_table = old_table.clone();
668 new_table
669 .add_domain(DomainInfo::new("Location", 50))
670 .expect("unwrap");
671 new_table
672 .add_domain(DomainInfo::new("Event", 30))
673 .expect("unwrap");
674
675 let diff = compute_diff(&old_table, &new_table);
676 let summary = diff.summary();
677 assert_eq!(summary.total_changes(), 2);
678 }
679}