1use std::collections::HashMap;
9
10use panproto_gat::Name;
11use panproto_schema::{Constraint, Edge, RecursionPoint, Schema, UsageMode, Variant};
12use rustc_hash::FxHashSet;
13use serde::{Deserialize, Serialize};
14
15#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
20pub struct SchemaDiff {
21 pub added_vertices: Vec<String>,
24 pub removed_vertices: Vec<String>,
26 pub kind_changes: Vec<KindChange>,
28
29 pub added_edges: Vec<Edge>,
32 pub removed_edges: Vec<Edge>,
34
35 pub modified_constraints: HashMap<String, ConstraintDiff>,
38
39 pub added_hyper_edges: Vec<String>,
42 pub removed_hyper_edges: Vec<String>,
44 pub modified_hyper_edges: Vec<HyperEdgeChange>,
46
47 pub added_required: HashMap<String, Vec<Edge>>,
50 pub removed_required: HashMap<String, Vec<Edge>>,
52
53 pub added_nsids: HashMap<String, String>,
56 pub removed_nsids: Vec<String>,
58 pub changed_nsids: Vec<(String, String, String)>,
60
61 pub added_variants: Vec<Variant>,
64 pub removed_variants: Vec<Variant>,
66 pub modified_variants: Vec<VariantChange>,
68
69 pub order_changes: Vec<(Edge, Option<u32>, Option<u32>)>,
72
73 pub added_recursion_points: Vec<(Name, RecursionPoint)>,
80 pub removed_recursion_points: Vec<(Name, RecursionPoint)>,
82 pub modified_recursion_points: Vec<RecursionPointChange>,
84
85 pub usage_mode_changes: Vec<(Edge, UsageMode, UsageMode)>,
88
89 pub added_spans: Vec<String>,
92 pub removed_spans: Vec<String>,
94 pub modified_spans: Vec<SpanChange>,
96
97 pub nominal_changes: Vec<(String, bool, bool)>,
100
101 #[serde(default, skip_serializing_if = "Vec::is_empty")]
104 pub added_coercions: Vec<(String, String)>,
105 #[serde(default, skip_serializing_if = "Vec::is_empty")]
107 pub removed_coercions: Vec<(String, String)>,
108 #[serde(default, skip_serializing_if = "Vec::is_empty")]
110 pub modified_coercions: Vec<(String, String)>,
111
112 #[serde(default, skip_serializing_if = "Vec::is_empty")]
114 pub added_mergers: Vec<String>,
115 #[serde(default, skip_serializing_if = "Vec::is_empty")]
117 pub removed_mergers: Vec<String>,
118 #[serde(default, skip_serializing_if = "Vec::is_empty")]
120 pub modified_mergers: Vec<String>,
121
122 #[serde(default, skip_serializing_if = "Vec::is_empty")]
124 pub added_defaults: Vec<String>,
125 #[serde(default, skip_serializing_if = "Vec::is_empty")]
127 pub removed_defaults: Vec<String>,
128 #[serde(default, skip_serializing_if = "Vec::is_empty")]
130 pub modified_defaults: Vec<String>,
131
132 #[serde(default, skip_serializing_if = "Vec::is_empty")]
134 pub added_policies: Vec<String>,
135 #[serde(default, skip_serializing_if = "Vec::is_empty")]
137 pub removed_policies: Vec<String>,
138 #[serde(default, skip_serializing_if = "Vec::is_empty")]
140 pub modified_policies: Vec<String>,
141
142 #[serde(default, skip_serializing_if = "Vec::is_empty")]
148 pub renamed_vertices: Vec<(String, String)>,
149}
150
151pub fn apply_renames(diff: &mut SchemaDiff, renames: &[(String, String)]) {
165 for (old_id, new_id) in renames {
166 let has_removed = diff.removed_vertices.iter().any(|v| v == old_id);
167 let has_added = diff.added_vertices.iter().any(|v| v == new_id);
168 if has_removed && has_added {
169 diff.removed_vertices.retain(|v| v != old_id);
170 diff.added_vertices.retain(|v| v != new_id);
171 diff.renamed_vertices.push((old_id.clone(), new_id.clone()));
172 }
173 }
174}
175
176#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
178pub struct ConstraintDiff {
179 pub added: Vec<Constraint>,
181 pub removed: Vec<Constraint>,
183 pub changed: Vec<ConstraintChange>,
185}
186
187#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
189pub struct ConstraintChange {
190 pub sort: String,
192 pub old_value: String,
194 pub new_value: String,
196}
197
198#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
200pub struct KindChange {
201 pub vertex_id: String,
203 pub old_kind: String,
205 pub new_kind: String,
207}
208
209#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
211pub struct HyperEdgeChange {
212 pub id: String,
214 pub kind_change: Option<(String, String)>,
216 pub signature_added: HashMap<String, String>,
218 pub signature_removed: HashMap<String, String>,
220 pub signature_changed: HashMap<String, (String, String)>,
222 pub parent_label_change: Option<(String, String)>,
224}
225
226#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
228pub struct VariantChange {
229 pub id: String,
231 pub parent_vertex: String,
233 pub old_tag: Option<String>,
235 pub new_tag: Option<String>,
237}
238
239#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
241pub struct RecursionPointChange {
242 pub mu_id: String,
244 pub old_target: String,
246 pub new_target: String,
248}
249
250#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
252pub struct SpanChange {
253 pub id: String,
255 pub left_change: Option<(String, String)>,
257 pub right_change: Option<(String, String)>,
259}
260
261#[must_use]
269#[allow(clippy::too_many_lines)]
270pub fn diff(old: &Schema, new: &Schema) -> SchemaDiff {
271 let mut result = SchemaDiff::default();
272
273 let old_verts: FxHashSet<&panproto_gat::Name> = old.vertices.keys().collect();
275 let new_verts: FxHashSet<&panproto_gat::Name> = new.vertices.keys().collect();
276
277 for v in &new_verts {
278 if !old_verts.contains(*v) {
279 result.added_vertices.push(v.to_string());
280 }
281 }
282 for v in &old_verts {
283 if !new_verts.contains(*v) {
284 result.removed_vertices.push(v.to_string());
285 }
286 }
287 result.added_vertices.sort();
288 result.removed_vertices.sort();
289
290 for v in old_verts.intersection(&new_verts) {
292 if let (Some(old_v), Some(new_v)) = (old.vertices.get(*v), new.vertices.get(*v)) {
293 if old_v.kind != new_v.kind {
294 result.kind_changes.push(KindChange {
295 vertex_id: v.to_string(),
296 old_kind: old_v.kind.to_string(),
297 new_kind: new_v.kind.to_string(),
298 });
299 }
300 }
301 }
302 result
303 .kind_changes
304 .sort_by(|a, b| a.vertex_id.cmp(&b.vertex_id));
305
306 let old_edges: FxHashSet<&Edge> = old.edges.keys().collect();
308 let new_edges: FxHashSet<&Edge> = new.edges.keys().collect();
309
310 for e in &new_edges {
311 if !old_edges.contains(*e) {
312 result.added_edges.push((*e).clone());
313 }
314 }
315 for e in &old_edges {
316 if !new_edges.contains(*e) {
317 result.removed_edges.push((*e).clone());
318 }
319 }
320 result.added_edges.sort();
321 result.removed_edges.sort();
322
323 let all_constraint_vids: FxHashSet<&panproto_gat::Name> = old
325 .constraints
326 .keys()
327 .chain(new.constraints.keys())
328 .collect();
329
330 for vid in all_constraint_vids {
331 if !old_verts.contains(vid) || !new_verts.contains(vid) {
337 continue;
338 }
339
340 let old_cs = old.constraints.get(vid).cloned().unwrap_or_default();
341 let new_cs = new.constraints.get(vid).cloned().unwrap_or_default();
342
343 let cdiff = diff_constraints(&old_cs, &new_cs);
344 if !cdiff.added.is_empty() || !cdiff.removed.is_empty() || !cdiff.changed.is_empty() {
345 result.modified_constraints.insert(vid.to_string(), cdiff);
346 }
347 }
348
349 diff_hyper_edges(old, new, &mut result);
351
352 diff_required(old, new, &mut result);
354
355 diff_nsids(old, new, &mut result);
357
358 diff_variants(old, new, &mut result);
360
361 let all_order_edges: FxHashSet<&Edge> =
363 old.orderings.keys().chain(new.orderings.keys()).collect();
364 for edge in all_order_edges {
365 let old_pos = old.orderings.get(edge).copied();
366 let new_pos = new.orderings.get(edge).copied();
367 if old_pos != new_pos {
368 result
369 .order_changes
370 .push(((*edge).clone(), old_pos, new_pos));
371 }
372 }
373
374 diff_recursion_points(old, new, &mut result);
376
377 let all_usage_edges: FxHashSet<&Edge> = old
379 .usage_modes
380 .keys()
381 .chain(new.usage_modes.keys())
382 .collect();
383 for edge in all_usage_edges {
384 let old_mode = old.usage_modes.get(edge).cloned().unwrap_or_default();
385 let new_mode = new.usage_modes.get(edge).cloned().unwrap_or_default();
386 if old_mode != new_mode {
387 result
388 .usage_mode_changes
389 .push(((*edge).clone(), old_mode, new_mode));
390 }
391 }
392
393 diff_spans(old, new, &mut result);
395
396 diff_nominal(old, new, &mut result);
398
399 diff_coercions(old, new, &mut result);
401 diff_name_keyed_exprs(
402 &old.mergers,
403 &new.mergers,
404 &mut result.added_mergers,
405 &mut result.removed_mergers,
406 &mut result.modified_mergers,
407 );
408 diff_name_keyed_exprs(
409 &old.defaults,
410 &new.defaults,
411 &mut result.added_defaults,
412 &mut result.removed_defaults,
413 &mut result.modified_defaults,
414 );
415 diff_name_keyed_exprs(
416 &old.policies,
417 &new.policies,
418 &mut result.added_policies,
419 &mut result.removed_policies,
420 &mut result.modified_policies,
421 );
422
423 result
424}
425
426fn diff_constraints(old: &[Constraint], new: &[Constraint]) -> ConstraintDiff {
432 let mut added = Vec::new();
433 let mut removed = Vec::new();
434 let mut changed = Vec::new();
435
436 let old_by_sort: HashMap<&str, &Constraint> =
437 old.iter().map(|c| (c.sort.as_str(), c)).collect();
438 let new_by_sort: HashMap<&str, &Constraint> =
439 new.iter().map(|c| (c.sort.as_str(), c)).collect();
440
441 for (sort, nc) in &new_by_sort {
442 match old_by_sort.get(sort) {
443 Some(oc) if oc.value != nc.value => {
444 changed.push(ConstraintChange {
445 sort: sort.to_string(),
446 old_value: oc.value.clone(),
447 new_value: nc.value.clone(),
448 });
449 }
450 None => {
451 added.push((*nc).clone());
452 }
453 _ => {}
454 }
455 }
456
457 for (sort, oc) in &old_by_sort {
458 if !new_by_sort.contains_key(sort) {
459 removed.push((*oc).clone());
460 }
461 }
462
463 ConstraintDiff {
464 added,
465 removed,
466 changed,
467 }
468}
469
470fn diff_hyper_edges(old: &Schema, new: &Schema, result: &mut SchemaDiff) {
472 let old_ids: FxHashSet<&panproto_gat::Name> = old.hyper_edges.keys().collect();
473 let new_ids: FxHashSet<&panproto_gat::Name> = new.hyper_edges.keys().collect();
474
475 for id in &new_ids {
476 if !old_ids.contains(*id) {
477 result.added_hyper_edges.push(id.to_string());
478 }
479 }
480 for id in &old_ids {
481 if !new_ids.contains(*id) {
482 result.removed_hyper_edges.push(id.to_string());
483 }
484 }
485 result.added_hyper_edges.sort();
486 result.removed_hyper_edges.sort();
487
488 for id in old_ids.intersection(&new_ids) {
490 let old_he = &old.hyper_edges[*id];
491 let new_he = &new.hyper_edges[*id];
492
493 if old_he == new_he {
494 continue;
495 }
496
497 let kind_change = if old_he.kind == new_he.kind {
498 None
499 } else {
500 Some((old_he.kind.to_string(), new_he.kind.to_string()))
501 };
502
503 let parent_label_change = if old_he.parent_label == new_he.parent_label {
504 None
505 } else {
506 Some((
507 old_he.parent_label.to_string(),
508 new_he.parent_label.to_string(),
509 ))
510 };
511
512 let mut sig_added = HashMap::new();
513 let mut sig_removed = HashMap::new();
514 let mut sig_changed = HashMap::new();
515
516 for (label, new_vid) in &new_he.signature {
517 match old_he.signature.get(label) {
518 Some(old_vid) if old_vid != new_vid => {
519 sig_changed.insert(
520 label.to_string(),
521 (old_vid.to_string(), new_vid.to_string()),
522 );
523 }
524 None => {
525 sig_added.insert(label.to_string(), new_vid.to_string());
526 }
527 _ => {}
528 }
529 }
530 for (label, old_vid) in &old_he.signature {
531 if !new_he.signature.contains_key(label) {
532 sig_removed.insert(label.to_string(), old_vid.to_string());
533 }
534 }
535
536 result.modified_hyper_edges.push(HyperEdgeChange {
537 id: id.to_string(),
538 kind_change,
539 signature_added: sig_added,
540 signature_removed: sig_removed,
541 signature_changed: sig_changed,
542 parent_label_change,
543 });
544 }
545 result.modified_hyper_edges.sort_by(|a, b| a.id.cmp(&b.id));
546}
547
548fn diff_required(old: &Schema, new: &Schema, result: &mut SchemaDiff) {
550 let all_vids: FxHashSet<&panproto_gat::Name> =
551 old.required.keys().chain(new.required.keys()).collect();
552
553 for vid in all_vids {
554 let old_edges: FxHashSet<&Edge> = old
555 .required
556 .get(vid)
557 .map(|v| v.iter().collect())
558 .unwrap_or_default();
559 let new_edges: FxHashSet<&Edge> = new
560 .required
561 .get(vid)
562 .map(|v| v.iter().collect())
563 .unwrap_or_default();
564
565 let added: Vec<Edge> = new_edges
566 .difference(&old_edges)
567 .map(|e| (*e).clone())
568 .collect();
569 let removed: Vec<Edge> = old_edges
570 .difference(&new_edges)
571 .map(|e| (*e).clone())
572 .collect();
573
574 if !added.is_empty() {
575 result.added_required.insert(vid.to_string(), added);
576 }
577 if !removed.is_empty() {
578 result.removed_required.insert(vid.to_string(), removed);
579 }
580 }
581}
582
583fn diff_nsids(old: &Schema, new: &Schema, result: &mut SchemaDiff) {
585 for (vid, new_nsid) in &new.nsids {
586 match old.nsids.get(vid) {
587 Some(old_nsid) if old_nsid != new_nsid => {
588 result.changed_nsids.push((
589 vid.to_string(),
590 old_nsid.to_string(),
591 new_nsid.to_string(),
592 ));
593 }
594 None => {
595 result
596 .added_nsids
597 .insert(vid.to_string(), new_nsid.to_string());
598 }
599 _ => {}
600 }
601 }
602 for vid in old.nsids.keys() {
603 if !new.nsids.contains_key(vid) {
604 result.removed_nsids.push(vid.to_string());
605 }
606 }
607 result.removed_nsids.sort();
608 result.changed_nsids.sort_by(|a, b| a.0.cmp(&b.0));
609}
610
611fn diff_variants(old: &Schema, new: &Schema, result: &mut SchemaDiff) {
613 let mut old_flat: HashMap<(&str, &str), &Variant> = HashMap::new();
615 let mut new_flat: HashMap<(&str, &str), &Variant> = HashMap::new();
616
617 for (parent, variants) in &old.variants {
618 for v in variants {
619 old_flat.insert((parent.as_str(), v.id.as_str()), v);
620 }
621 }
622 for (parent, variants) in &new.variants {
623 for v in variants {
624 new_flat.insert((parent.as_str(), v.id.as_str()), v);
625 }
626 }
627
628 for (&(parent, vid), new_v) in &new_flat {
630 match old_flat.get(&(parent, vid)) {
631 Some(old_v) => {
632 if old_v.tag != new_v.tag {
633 result.modified_variants.push(VariantChange {
634 id: vid.to_string(),
635 parent_vertex: parent.to_string(),
636 old_tag: old_v.tag.as_ref().map(ToString::to_string),
637 new_tag: new_v.tag.as_ref().map(ToString::to_string),
638 });
639 }
640 }
641 None => {
642 result.added_variants.push((*new_v).clone());
643 }
644 }
645 }
646
647 for (&(parent, vid), old_v) in &old_flat {
649 if !new_flat.contains_key(&(parent, vid)) {
650 result.removed_variants.push((*old_v).clone());
651 }
652 }
653
654 result
655 .modified_variants
656 .sort_by(|a, b| (&a.parent_vertex, &a.id).cmp(&(&b.parent_vertex, &b.id)));
657}
658
659fn diff_recursion_points(old: &Schema, new: &Schema, result: &mut SchemaDiff) {
661 for (id, new_rp) in &new.recursion_points {
662 match old.recursion_points.get(id) {
663 Some(old_rp) => {
664 if old_rp.target_vertex != new_rp.target_vertex {
665 result.modified_recursion_points.push(RecursionPointChange {
666 mu_id: id.to_string(),
667 old_target: old_rp.target_vertex.to_string(),
668 new_target: new_rp.target_vertex.to_string(),
669 });
670 }
671 }
672 None => {
673 result
674 .added_recursion_points
675 .push((id.clone(), new_rp.clone()));
676 }
677 }
678 }
679 for (id, old_rp) in &old.recursion_points {
680 if !new.recursion_points.contains_key(id) {
681 result
682 .removed_recursion_points
683 .push((id.clone(), old_rp.clone()));
684 }
685 }
686}
687
688fn diff_spans(old: &Schema, new: &Schema, result: &mut SchemaDiff) {
690 let old_ids: FxHashSet<&panproto_gat::Name> = old.spans.keys().collect();
691 let new_ids: FxHashSet<&panproto_gat::Name> = new.spans.keys().collect();
692
693 for id in &new_ids {
694 if !old_ids.contains(*id) {
695 result.added_spans.push(id.to_string());
696 }
697 }
698 for id in &old_ids {
699 if !new_ids.contains(*id) {
700 result.removed_spans.push(id.to_string());
701 }
702 }
703 result.added_spans.sort();
704 result.removed_spans.sort();
705
706 for id in old_ids.intersection(&new_ids) {
707 let old_span = &old.spans[*id];
708 let new_span = &new.spans[*id];
709
710 if old_span == new_span {
711 continue;
712 }
713
714 let left_change = if old_span.left == new_span.left {
715 None
716 } else {
717 Some((old_span.left.to_string(), new_span.left.to_string()))
718 };
719 let right_change = if old_span.right == new_span.right {
720 None
721 } else {
722 Some((old_span.right.to_string(), new_span.right.to_string()))
723 };
724
725 result.modified_spans.push(SpanChange {
726 id: id.to_string(),
727 left_change,
728 right_change,
729 });
730 }
731 result.modified_spans.sort_by(|a, b| a.id.cmp(&b.id));
732}
733
734fn diff_nominal(old: &Schema, new: &Schema, result: &mut SchemaDiff) {
736 let all_vids: FxHashSet<&panproto_gat::Name> =
737 old.nominal.keys().chain(new.nominal.keys()).collect();
738
739 for vid in all_vids {
740 let old_val = old.nominal.get(vid).copied().unwrap_or(false);
741 let new_val = new.nominal.get(vid).copied().unwrap_or(false);
742 if old_val != new_val {
743 result
744 .nominal_changes
745 .push((vid.to_string(), old_val, new_val));
746 }
747 }
748 result.nominal_changes.sort_by(|a, b| a.0.cmp(&b.0));
749}
750
751fn diff_coercions(old: &Schema, new: &Schema, result: &mut SchemaDiff) {
753 for (key, new_expr) in &new.coercions {
754 match old.coercions.get(key) {
755 Some(old_expr) => {
756 if old_expr != new_expr {
757 result
758 .modified_coercions
759 .push((key.0.to_string(), key.1.to_string()));
760 }
761 }
762 None => {
763 result
764 .added_coercions
765 .push((key.0.to_string(), key.1.to_string()));
766 }
767 }
768 }
769 for key in old.coercions.keys() {
770 if !new.coercions.contains_key(key) {
771 result
772 .removed_coercions
773 .push((key.0.to_string(), key.1.to_string()));
774 }
775 }
776 result.added_coercions.sort();
777 result.removed_coercions.sort();
778 result.modified_coercions.sort();
779}
780
781fn diff_name_keyed_exprs<V: PartialEq>(
783 old: &std::collections::HashMap<panproto_gat::Name, V>,
784 new: &std::collections::HashMap<panproto_gat::Name, V>,
785 added: &mut Vec<String>,
786 removed: &mut Vec<String>,
787 modified: &mut Vec<String>,
788) {
789 for (key, new_expr) in new {
790 match old.get(key) {
791 Some(old_expr) => {
792 if old_expr != new_expr {
793 modified.push(key.to_string());
794 }
795 }
796 None => {
797 added.push(key.to_string());
798 }
799 }
800 }
801 for key in old.keys() {
802 if !new.contains_key(key) {
803 removed.push(key.to_string());
804 }
805 }
806 added.sort();
807 removed.sort();
808 modified.sort();
809}
810
811impl SchemaDiff {
812 #[must_use]
814 pub fn is_empty(&self) -> bool {
815 self.added_vertices.is_empty()
816 && self.removed_vertices.is_empty()
817 && self.kind_changes.is_empty()
818 && self.added_edges.is_empty()
819 && self.removed_edges.is_empty()
820 && self.modified_constraints.is_empty()
821 && self.added_hyper_edges.is_empty()
822 && self.removed_hyper_edges.is_empty()
823 && self.modified_hyper_edges.is_empty()
824 && self.added_required.is_empty()
825 && self.removed_required.is_empty()
826 && self.added_nsids.is_empty()
827 && self.removed_nsids.is_empty()
828 && self.changed_nsids.is_empty()
829 && self.added_variants.is_empty()
830 && self.removed_variants.is_empty()
831 && self.modified_variants.is_empty()
832 && self.order_changes.is_empty()
833 && self.added_recursion_points.is_empty()
834 && self.removed_recursion_points.is_empty()
835 && self.modified_recursion_points.is_empty()
836 && self.usage_mode_changes.is_empty()
837 && self.added_spans.is_empty()
838 && self.removed_spans.is_empty()
839 && self.modified_spans.is_empty()
840 && self.nominal_changes.is_empty()
841 && self.added_coercions.is_empty()
842 && self.removed_coercions.is_empty()
843 && self.modified_coercions.is_empty()
844 && self.added_mergers.is_empty()
845 && self.removed_mergers.is_empty()
846 && self.modified_mergers.is_empty()
847 && self.added_defaults.is_empty()
848 && self.removed_defaults.is_empty()
849 && self.modified_defaults.is_empty()
850 && self.added_policies.is_empty()
851 && self.removed_policies.is_empty()
852 && self.modified_policies.is_empty()
853 && self.renamed_vertices.is_empty()
854 }
855}
856
857#[cfg(test)]
858mod tests {
859 use super::*;
860 use panproto_gat::Name;
861 use panproto_schema::{HyperEdge, RecursionPoint, Span, Variant, Vertex};
862 use smallvec::SmallVec;
863 use std::collections::HashMap;
864
865 fn test_schema(
867 vertices: &[(&str, &str)],
868 edges: &[Edge],
869 constraints: HashMap<Name, Vec<Constraint>>,
870 ) -> Schema {
871 let mut vert_map = HashMap::new();
872 let mut edge_map = HashMap::new();
873 let mut outgoing: HashMap<Name, SmallVec<Edge, 4>> = HashMap::new();
874 let mut incoming: HashMap<Name, SmallVec<Edge, 4>> = HashMap::new();
875 let mut between: HashMap<(Name, Name), SmallVec<Edge, 2>> = HashMap::new();
876
877 for (id, kind) in vertices {
878 vert_map.insert(
879 Name::from(*id),
880 Vertex {
881 id: Name::from(*id),
882 kind: Name::from(*kind),
883 nsid: None,
884 },
885 );
886 }
887
888 for edge in edges {
889 edge_map.insert(edge.clone(), edge.kind.clone());
890 outgoing
891 .entry(edge.src.clone())
892 .or_default()
893 .push(edge.clone());
894 incoming
895 .entry(edge.tgt.clone())
896 .or_default()
897 .push(edge.clone());
898 between
899 .entry((edge.src.clone(), edge.tgt.clone()))
900 .or_default()
901 .push(edge.clone());
902 }
903
904 Schema {
905 protocol: "test".into(),
906 vertices: vert_map,
907 edges: edge_map,
908 hyper_edges: HashMap::new(),
909 constraints,
910 required: HashMap::new(),
911 nsids: HashMap::new(),
912 entries: Vec::new(),
913 variants: HashMap::new(),
914 orderings: HashMap::new(),
915 recursion_points: HashMap::new(),
916 spans: HashMap::new(),
917 usage_modes: HashMap::new(),
918 nominal: HashMap::new(),
919 coercions: HashMap::new(),
920 mergers: HashMap::new(),
921 defaults: HashMap::new(),
922 policies: HashMap::new(),
923 outgoing,
924 incoming,
925 between,
926 }
927 }
928
929 fn test_schema_ext(base: Schema, f: impl FnOnce(&mut Schema)) -> Schema {
931 let mut s = base;
932 f(&mut s);
933 s
934 }
935
936 #[test]
941 fn diff_added_and_removed_vertices() {
942 let edge = Edge {
943 src: "a".into(),
944 tgt: "b".into(),
945 kind: "prop".into(),
946 name: None,
947 };
948 let old = test_schema(&[("a", "object"), ("b", "string")], &[edge], HashMap::new());
949 let new = test_schema(&[("a", "object"), ("c", "integer")], &[], HashMap::new());
950
951 let d = diff(&old, &new);
952 assert_eq!(d.added_vertices, vec!["c"]);
953 assert_eq!(d.removed_vertices, vec!["b"]);
954 assert_eq!(d.removed_edges.len(), 1);
955 }
956
957 #[test]
958 fn diff_kind_change() {
959 let old = test_schema(&[("x", "string")], &[], HashMap::new());
960 let new = test_schema(&[("x", "integer")], &[], HashMap::new());
961
962 let d = diff(&old, &new);
963 assert_eq!(d.kind_changes.len(), 1);
964 assert_eq!(d.kind_changes[0].old_kind, "string");
965 assert_eq!(d.kind_changes[0].new_kind, "integer");
966 }
967
968 #[test]
969 fn diff_constraint_changed() {
970 let old_constraints = HashMap::from([(
971 Name::from("x"),
972 vec![Constraint {
973 sort: "maxLength".into(),
974 value: "3000".into(),
975 }],
976 )]);
977 let new_constraints = HashMap::from([(
978 Name::from("x"),
979 vec![Constraint {
980 sort: "maxLength".into(),
981 value: "300".into(),
982 }],
983 )]);
984
985 let old = test_schema(&[("x", "string")], &[], old_constraints);
986 let new = test_schema(&[("x", "string")], &[], new_constraints);
987
988 let d = diff(&old, &new);
989 assert!(d.modified_constraints.contains_key("x"));
990 let cdiff = &d.modified_constraints["x"];
991 assert_eq!(cdiff.changed.len(), 1);
992 assert_eq!(cdiff.changed[0].old_value, "3000");
993 assert_eq!(cdiff.changed[0].new_value, "300");
994 }
995
996 #[test]
997 fn diff_constraint_on_added_vertex_is_not_a_modification() {
998 let old = test_schema(&[("post", "record")], &[], HashMap::new());
1003 let new_constraints = HashMap::from([(
1004 Name::from("title"),
1005 vec![Constraint {
1006 sort: "maxLength".into(),
1007 value: "120".into(),
1008 }],
1009 )]);
1010 let new = test_schema(
1011 &[("post", "record"), ("title", "string")],
1012 &[],
1013 new_constraints,
1014 );
1015
1016 let d = diff(&old, &new);
1017 assert_eq!(d.added_vertices, vec!["title"]);
1018 assert!(
1019 !d.modified_constraints.contains_key("title"),
1020 "a constraint on a newly-added vertex is not a modification"
1021 );
1022 }
1023
1024 #[test]
1025 fn empty_diff_for_identical_schemas() {
1026 let s = test_schema(&[("a", "object")], &[], HashMap::new());
1027 let d = diff(&s, &s);
1028 assert!(d.is_empty());
1029 }
1030
1031 #[test]
1036 fn diff_hyper_edge_added() {
1037 let base = test_schema(&[("a", "object")], &[], HashMap::new());
1038 let new = test_schema_ext(base.clone(), |s| {
1039 s.hyper_edges.insert(
1040 "he1".into(),
1041 HyperEdge {
1042 id: "he1".into(),
1043 kind: "join".into(),
1044 signature: HashMap::from([("left".into(), "a".into())]),
1045 parent_label: "left".into(),
1046 },
1047 );
1048 });
1049 let d = diff(&base, &new);
1050 assert_eq!(d.added_hyper_edges, vec!["he1"]);
1051 assert!(d.removed_hyper_edges.is_empty());
1052 assert!(!d.is_empty());
1053 }
1054
1055 #[test]
1056 fn diff_hyper_edge_removed() {
1057 let old = test_schema_ext(test_schema(&[("a", "object")], &[], HashMap::new()), |s| {
1058 s.hyper_edges.insert(
1059 "he1".into(),
1060 HyperEdge {
1061 id: "he1".into(),
1062 kind: "join".into(),
1063 signature: HashMap::from([("left".into(), "a".into())]),
1064 parent_label: "left".into(),
1065 },
1066 );
1067 });
1068 let new = test_schema(&[("a", "object")], &[], HashMap::new());
1069 let d = diff(&old, &new);
1070 assert_eq!(d.removed_hyper_edges, vec!["he1"]);
1071 }
1072
1073 #[test]
1074 fn diff_hyper_edge_modified_kind() {
1075 let he = HyperEdge {
1076 id: "he1".into(),
1077 kind: "join".into(),
1078 signature: HashMap::from([("left".into(), "a".into())]),
1079 parent_label: "left".into(),
1080 };
1081 let old = test_schema_ext(test_schema(&[("a", "object")], &[], HashMap::new()), |s| {
1082 s.hyper_edges.insert("he1".into(), he.clone());
1083 });
1084 let new = test_schema_ext(test_schema(&[("a", "object")], &[], HashMap::new()), |s| {
1085 let mut he2 = he.clone();
1086 he2.kind = "merge".into();
1087 s.hyper_edges.insert("he1".into(), he2);
1088 });
1089 let d = diff(&old, &new);
1090 assert_eq!(d.modified_hyper_edges.len(), 1);
1091 assert_eq!(
1092 d.modified_hyper_edges[0].kind_change,
1093 Some(("join".into(), "merge".into()))
1094 );
1095 }
1096
1097 #[test]
1098 fn diff_hyper_edge_modified_signature() {
1099 let old = test_schema_ext(
1100 test_schema(&[("a", "object"), ("b", "string")], &[], HashMap::new()),
1101 |s| {
1102 s.hyper_edges.insert(
1103 "he1".into(),
1104 HyperEdge {
1105 id: "he1".into(),
1106 kind: "join".into(),
1107 signature: HashMap::from([("left".into(), "a".into())]),
1108 parent_label: "left".into(),
1109 },
1110 );
1111 },
1112 );
1113 let new = test_schema_ext(
1114 test_schema(&[("a", "object"), ("b", "string")], &[], HashMap::new()),
1115 |s| {
1116 s.hyper_edges.insert(
1117 "he1".into(),
1118 HyperEdge {
1119 id: "he1".into(),
1120 kind: "join".into(),
1121 signature: HashMap::from([
1122 ("left".into(), "a".into()),
1123 ("right".into(), "b".into()),
1124 ]),
1125 parent_label: "left".into(),
1126 },
1127 );
1128 },
1129 );
1130 let d = diff(&old, &new);
1131 assert_eq!(d.modified_hyper_edges.len(), 1);
1132 assert_eq!(
1133 d.modified_hyper_edges[0].signature_added.get("right"),
1134 Some(&"b".to_string())
1135 );
1136 }
1137
1138 #[test]
1143 fn diff_required_edge_added() {
1144 let edge = Edge {
1145 src: "a".into(),
1146 tgt: "b".into(),
1147 kind: "prop".into(),
1148 name: Some("x".into()),
1149 };
1150 let base = test_schema(
1151 &[("a", "object"), ("b", "string")],
1152 std::slice::from_ref(&edge),
1153 HashMap::new(),
1154 );
1155 let new = test_schema_ext(base.clone(), |s| {
1156 s.required.insert("a".into(), vec![edge.clone()]);
1157 });
1158 let d = diff(&base, &new);
1159 assert_eq!(d.added_required.len(), 1);
1160 assert_eq!(d.added_required["a"].len(), 1);
1161 }
1162
1163 #[test]
1164 fn diff_required_edge_removed() {
1165 let edge = Edge {
1166 src: "a".into(),
1167 tgt: "b".into(),
1168 kind: "prop".into(),
1169 name: Some("x".into()),
1170 };
1171 let old = test_schema_ext(
1172 test_schema(
1173 &[("a", "object"), ("b", "string")],
1174 std::slice::from_ref(&edge),
1175 HashMap::new(),
1176 ),
1177 |s| {
1178 s.required.insert("a".into(), vec![edge.clone()]);
1179 },
1180 );
1181 let new = test_schema(&[("a", "object"), ("b", "string")], &[edge], HashMap::new());
1182 let d = diff(&old, &new);
1183 assert_eq!(d.removed_required.len(), 1);
1184 assert_eq!(d.removed_required["a"].len(), 1);
1185 }
1186
1187 #[test]
1192 fn diff_nsid_added() {
1193 let base = test_schema(&[("a", "object")], &[], HashMap::new());
1194 let new = test_schema_ext(base.clone(), |s| {
1195 s.nsids.insert("a".into(), "com.example.thing".into());
1196 });
1197 let d = diff(&base, &new);
1198 assert_eq!(
1199 d.added_nsids.get("a"),
1200 Some(&"com.example.thing".to_string())
1201 );
1202 }
1203
1204 #[test]
1205 fn diff_nsid_removed() {
1206 let old = test_schema_ext(test_schema(&[("a", "object")], &[], HashMap::new()), |s| {
1207 s.nsids.insert("a".into(), "com.example.thing".into());
1208 });
1209 let new = test_schema(&[("a", "object")], &[], HashMap::new());
1210 let d = diff(&old, &new);
1211 assert_eq!(d.removed_nsids, vec!["a"]);
1212 }
1213
1214 #[test]
1215 fn diff_nsid_changed() {
1216 let old = test_schema_ext(test_schema(&[("a", "object")], &[], HashMap::new()), |s| {
1217 s.nsids.insert("a".into(), "com.example.old".into());
1218 });
1219 let new = test_schema_ext(test_schema(&[("a", "object")], &[], HashMap::new()), |s| {
1220 s.nsids.insert("a".into(), "com.example.new".into());
1221 });
1222 let d = diff(&old, &new);
1223 assert_eq!(d.changed_nsids.len(), 1);
1224 assert_eq!(
1225 d.changed_nsids[0],
1226 (
1227 "a".into(),
1228 "com.example.old".into(),
1229 "com.example.new".into()
1230 )
1231 );
1232 }
1233
1234 #[test]
1239 fn diff_variant_tag_modified() {
1240 let old = test_schema_ext(test_schema(&[("u", "union")], &[], HashMap::new()), |s| {
1241 s.variants.insert(
1242 "u".into(),
1243 vec![Variant {
1244 id: "v1".into(),
1245 parent_vertex: "u".into(),
1246 tag: Some("a".into()),
1247 }],
1248 );
1249 });
1250 let new = test_schema_ext(test_schema(&[("u", "union")], &[], HashMap::new()), |s| {
1251 s.variants.insert(
1252 "u".into(),
1253 vec![Variant {
1254 id: "v1".into(),
1255 parent_vertex: "u".into(),
1256 tag: Some("b".into()),
1257 }],
1258 );
1259 });
1260 let d = diff(&old, &new);
1261 assert!(d.added_variants.is_empty());
1262 assert!(d.removed_variants.is_empty());
1263 assert_eq!(d.modified_variants.len(), 1);
1264 assert_eq!(d.modified_variants[0].old_tag, Some("a".into()));
1265 assert_eq!(d.modified_variants[0].new_tag, Some("b".into()));
1266 }
1267
1268 #[test]
1273 fn diff_recursion_point_target_modified() {
1274 let old = test_schema_ext(
1275 test_schema(
1276 &[("a", "object"), ("b", "string"), ("c", "integer")],
1277 &[],
1278 HashMap::new(),
1279 ),
1280 |s| {
1281 s.recursion_points.insert(
1282 "mu1".into(),
1283 RecursionPoint {
1284 target_vertex: "b".into(),
1285 },
1286 );
1287 },
1288 );
1289 let new = test_schema_ext(
1290 test_schema(
1291 &[("a", "object"), ("b", "string"), ("c", "integer")],
1292 &[],
1293 HashMap::new(),
1294 ),
1295 |s| {
1296 s.recursion_points.insert(
1297 "mu1".into(),
1298 RecursionPoint {
1299 target_vertex: "c".into(),
1300 },
1301 );
1302 },
1303 );
1304 let d = diff(&old, &new);
1305 assert!(d.added_recursion_points.is_empty());
1306 assert!(d.removed_recursion_points.is_empty());
1307 assert_eq!(d.modified_recursion_points.len(), 1);
1308 assert_eq!(d.modified_recursion_points[0].old_target, "b");
1309 assert_eq!(d.modified_recursion_points[0].new_target, "c");
1310 }
1311
1312 #[test]
1317 fn diff_span_added() {
1318 let base = test_schema(&[("a", "object"), ("b", "string")], &[], HashMap::new());
1319 let new = test_schema_ext(base.clone(), |s| {
1320 s.spans.insert(
1321 "s1".into(),
1322 Span {
1323 id: "s1".into(),
1324 left: "a".into(),
1325 right: "b".into(),
1326 },
1327 );
1328 });
1329 let d = diff(&base, &new);
1330 assert_eq!(d.added_spans, vec!["s1"]);
1331 }
1332
1333 #[test]
1334 fn diff_span_modified() {
1335 let old = test_schema_ext(
1336 test_schema(
1337 &[("a", "object"), ("b", "string"), ("c", "integer")],
1338 &[],
1339 HashMap::new(),
1340 ),
1341 |s| {
1342 s.spans.insert(
1343 "s1".into(),
1344 Span {
1345 id: "s1".into(),
1346 left: "a".into(),
1347 right: "b".into(),
1348 },
1349 );
1350 },
1351 );
1352 let new = test_schema_ext(
1353 test_schema(
1354 &[("a", "object"), ("b", "string"), ("c", "integer")],
1355 &[],
1356 HashMap::new(),
1357 ),
1358 |s| {
1359 s.spans.insert(
1360 "s1".into(),
1361 Span {
1362 id: "s1".into(),
1363 left: "a".into(),
1364 right: "c".into(),
1365 },
1366 );
1367 },
1368 );
1369 let d = diff(&old, &new);
1370 assert_eq!(d.modified_spans.len(), 1);
1371 assert_eq!(
1372 d.modified_spans[0].right_change,
1373 Some(("b".into(), "c".into()))
1374 );
1375 assert_eq!(d.modified_spans[0].left_change, None);
1376 }
1377
1378 #[test]
1383 fn diff_nominal_changed() {
1384 let old = test_schema_ext(test_schema(&[("a", "object")], &[], HashMap::new()), |s| {
1385 s.nominal.insert("a".into(), false);
1386 });
1387 let new = test_schema_ext(test_schema(&[("a", "object")], &[], HashMap::new()), |s| {
1388 s.nominal.insert("a".into(), true);
1389 });
1390 let d = diff(&old, &new);
1391 assert_eq!(d.nominal_changes.len(), 1);
1392 assert_eq!(d.nominal_changes[0], ("a".into(), false, true));
1393 }
1394
1395 #[test]
1400 #[allow(clippy::too_many_lines)]
1401 fn is_empty_false_for_each_field() {
1402 let base = test_schema(&[("a", "object")], &[], HashMap::new());
1403
1404 let cases: Vec<SchemaDiff> = vec![
1406 SchemaDiff {
1407 added_vertices: vec!["x".into()],
1408 ..Default::default()
1409 },
1410 SchemaDiff {
1411 removed_vertices: vec!["x".into()],
1412 ..Default::default()
1413 },
1414 SchemaDiff {
1415 kind_changes: vec![KindChange {
1416 vertex_id: "a".into(),
1417 old_kind: "x".into(),
1418 new_kind: "y".into(),
1419 }],
1420 ..Default::default()
1421 },
1422 SchemaDiff {
1423 added_edges: vec![Edge {
1424 src: "a".into(),
1425 tgt: "b".into(),
1426 kind: "p".into(),
1427 name: None,
1428 }],
1429 ..Default::default()
1430 },
1431 SchemaDiff {
1432 removed_edges: vec![Edge {
1433 src: "a".into(),
1434 tgt: "b".into(),
1435 kind: "p".into(),
1436 name: None,
1437 }],
1438 ..Default::default()
1439 },
1440 SchemaDiff {
1441 modified_constraints: HashMap::from([(
1442 "a".into(),
1443 ConstraintDiff {
1444 added: vec![Constraint {
1445 sort: "s".into(),
1446 value: "v".into(),
1447 }],
1448 removed: vec![],
1449 changed: vec![],
1450 },
1451 )]),
1452 ..Default::default()
1453 },
1454 SchemaDiff {
1455 added_hyper_edges: vec!["he".into()],
1456 ..Default::default()
1457 },
1458 SchemaDiff {
1459 removed_hyper_edges: vec!["he".into()],
1460 ..Default::default()
1461 },
1462 SchemaDiff {
1463 modified_hyper_edges: vec![HyperEdgeChange {
1464 id: "he".into(),
1465 kind_change: None,
1466 signature_added: HashMap::new(),
1467 signature_removed: HashMap::new(),
1468 signature_changed: HashMap::new(),
1469 parent_label_change: Some(("a".into(), "b".into())),
1470 }],
1471 ..Default::default()
1472 },
1473 SchemaDiff {
1474 added_required: HashMap::from([("a".into(), vec![])]),
1475 ..Default::default()
1476 },
1477 SchemaDiff {
1478 removed_required: HashMap::from([("a".into(), vec![])]),
1479 ..Default::default()
1480 },
1481 SchemaDiff {
1482 added_nsids: HashMap::from([("a".into(), "x".into())]),
1483 ..Default::default()
1484 },
1485 SchemaDiff {
1486 removed_nsids: vec!["a".into()],
1487 ..Default::default()
1488 },
1489 SchemaDiff {
1490 changed_nsids: vec![("a".into(), "x".into(), "y".into())],
1491 ..Default::default()
1492 },
1493 SchemaDiff {
1494 added_variants: vec![Variant {
1495 id: "v".into(),
1496 parent_vertex: "u".into(),
1497 tag: None,
1498 }],
1499 ..Default::default()
1500 },
1501 SchemaDiff {
1502 removed_variants: vec![Variant {
1503 id: "v".into(),
1504 parent_vertex: "u".into(),
1505 tag: None,
1506 }],
1507 ..Default::default()
1508 },
1509 SchemaDiff {
1510 modified_variants: vec![VariantChange {
1511 id: "v".into(),
1512 parent_vertex: "u".into(),
1513 old_tag: None,
1514 new_tag: Some("t".into()),
1515 }],
1516 ..Default::default()
1517 },
1518 SchemaDiff {
1519 order_changes: vec![(
1520 Edge {
1521 src: "a".into(),
1522 tgt: "b".into(),
1523 kind: "p".into(),
1524 name: None,
1525 },
1526 Some(0),
1527 Some(1),
1528 )],
1529 ..Default::default()
1530 },
1531 SchemaDiff {
1532 added_recursion_points: vec![(
1533 "m".into(),
1534 RecursionPoint {
1535 target_vertex: "t".into(),
1536 },
1537 )],
1538 ..Default::default()
1539 },
1540 SchemaDiff {
1541 removed_recursion_points: vec![(
1542 "m".into(),
1543 RecursionPoint {
1544 target_vertex: "t".into(),
1545 },
1546 )],
1547 ..Default::default()
1548 },
1549 SchemaDiff {
1550 modified_recursion_points: vec![RecursionPointChange {
1551 mu_id: "m".into(),
1552 old_target: "a".into(),
1553 new_target: "b".into(),
1554 }],
1555 ..Default::default()
1556 },
1557 SchemaDiff {
1558 usage_mode_changes: vec![(
1559 Edge {
1560 src: "a".into(),
1561 tgt: "b".into(),
1562 kind: "p".into(),
1563 name: None,
1564 },
1565 UsageMode::Structural,
1566 UsageMode::Linear,
1567 )],
1568 ..Default::default()
1569 },
1570 SchemaDiff {
1571 added_spans: vec!["s".into()],
1572 ..Default::default()
1573 },
1574 SchemaDiff {
1575 removed_spans: vec!["s".into()],
1576 ..Default::default()
1577 },
1578 SchemaDiff {
1579 modified_spans: vec![SpanChange {
1580 id: "s".into(),
1581 left_change: Some(("a".into(), "b".into())),
1582 right_change: None,
1583 }],
1584 ..Default::default()
1585 },
1586 SchemaDiff {
1587 nominal_changes: vec![("a".into(), false, true)],
1588 ..Default::default()
1589 },
1590 SchemaDiff {
1591 added_coercions: vec![("a".into(), "b".into())],
1592 ..Default::default()
1593 },
1594 SchemaDiff {
1595 removed_coercions: vec![("a".into(), "b".into())],
1596 ..Default::default()
1597 },
1598 SchemaDiff {
1599 modified_coercions: vec![("a".into(), "b".into())],
1600 ..Default::default()
1601 },
1602 SchemaDiff {
1603 added_mergers: vec!["a".into()],
1604 ..Default::default()
1605 },
1606 SchemaDiff {
1607 removed_mergers: vec!["a".into()],
1608 ..Default::default()
1609 },
1610 SchemaDiff {
1611 modified_mergers: vec!["a".into()],
1612 ..Default::default()
1613 },
1614 SchemaDiff {
1615 added_defaults: vec!["a".into()],
1616 ..Default::default()
1617 },
1618 SchemaDiff {
1619 removed_defaults: vec!["a".into()],
1620 ..Default::default()
1621 },
1622 SchemaDiff {
1623 modified_defaults: vec!["a".into()],
1624 ..Default::default()
1625 },
1626 SchemaDiff {
1627 added_policies: vec!["a".into()],
1628 ..Default::default()
1629 },
1630 SchemaDiff {
1631 removed_policies: vec!["a".into()],
1632 ..Default::default()
1633 },
1634 SchemaDiff {
1635 modified_policies: vec!["a".into()],
1636 ..Default::default()
1637 },
1638 SchemaDiff {
1639 renamed_vertices: vec![("a".into(), "b".into())],
1640 ..Default::default()
1641 },
1642 ];
1643
1644 let _ = base; for (i, d) in cases.iter().enumerate() {
1646 assert!(!d.is_empty(), "case {i} should not be empty: {d:?}");
1647 }
1648 }
1649
1650 #[test]
1651 fn apply_renames_moves_pair_into_renamed_vertices() {
1652 let mut d = SchemaDiff {
1653 removed_vertices: vec!["root.text".into(), "root.gone".into()],
1654 added_vertices: vec!["root.body".into(), "root.new".into()],
1655 ..SchemaDiff::default()
1656 };
1657 apply_renames(&mut d, &[("root.text".into(), "root.body".into())]);
1658
1659 assert_eq!(
1660 d.renamed_vertices,
1661 vec![("root.text".into(), "root.body".into())]
1662 );
1663 assert_eq!(d.removed_vertices, vec!["root.gone".to_string()]);
1664 assert_eq!(d.added_vertices, vec!["root.new".to_string()]);
1665 }
1666
1667 #[test]
1668 fn apply_renames_ignores_unmatched_pairs() {
1669 let mut d = SchemaDiff {
1670 removed_vertices: vec!["root.text".into()],
1671 added_vertices: vec!["root.body".into()],
1672 ..SchemaDiff::default()
1673 };
1674 apply_renames(&mut d, &[("root.text".into(), "root.other".into())]);
1676 assert!(d.renamed_vertices.is_empty());
1677 assert_eq!(d.removed_vertices, vec!["root.text".to_string()]);
1678 }
1679}