use panproto_gat::Name;
use std::collections::HashMap;
use panproto_check::diff::SchemaDiff;
use panproto_mig::Migration;
use panproto_mig::hom_search::{SearchOptions, find_best_morphism, morphism_to_migration};
use panproto_schema::{Edge, Schema};
use rustc_hash::FxHashSet;
use crate::rename_detect;
#[must_use]
pub fn derive_migration(old: &Schema, new: &Schema, diff: &SchemaDiff) -> Migration {
let removed_verts: FxHashSet<&str> = diff.removed_vertices.iter().map(String::as_str).collect();
let removed_edges: FxHashSet<&Edge> = diff.removed_edges.iter().collect();
let vertex_map: HashMap<Name, Name> = old
.vertices
.keys()
.filter(|id| !removed_verts.contains(id.as_str()))
.map(|id| (id.clone(), id.clone()))
.collect();
let mut edge_map: HashMap<Edge, Edge> = HashMap::new();
for edge in old.edges.keys() {
if removed_edges.contains(edge) {
continue;
}
if removed_verts.contains(edge.src.as_str()) || removed_verts.contains(edge.tgt.as_str()) {
continue;
}
if new.edges.contains_key(edge) {
edge_map.insert(edge.clone(), edge.clone());
} else {
if let Some(matching) =
find_matching_edge(new, &edge.src, &edge.tgt, edge.name.as_deref())
{
edge_map.insert(edge.clone(), matching);
}
}
}
let hyper_edge_map: HashMap<Name, Name> = old
.hyper_edges
.keys()
.filter(|id| new.hyper_edges.contains_key(*id))
.map(|id| (id.clone(), id.clone()))
.collect();
let mut label_map: HashMap<(Name, Name), Name> = HashMap::new();
for (he_id, old_he) in &old.hyper_edges {
if let Some(new_he) = new.hyper_edges.get(he_id) {
for (label, vertex_id) in &old_he.signature {
if vertex_map.contains_key(vertex_id) {
if let Some(new_label) = find_label_for_vertex(new_he, vertex_id) {
label_map.insert((he_id.clone(), label.clone()), new_label);
}
}
}
}
}
let identity_mig = Migration {
vertex_map,
edge_map,
hyper_edge_map,
label_map,
resolver: HashMap::new(),
hyper_resolver: HashMap::new(),
expr_resolvers: HashMap::new(),
};
if !diff.removed_vertices.is_empty() && !diff.added_vertices.is_empty() {
if let Some(enhanced) = try_hom_search_enhancement(old, new, &identity_mig) {
return enhanced;
}
}
identity_mig
}
fn try_hom_search_enhancement(
old: &Schema,
new: &Schema,
identity_mig: &Migration,
) -> Option<Migration> {
let renames = rename_detect::detect_vertex_renames(old, new, 0.3);
let mut initial: HashMap<Name, Name> = HashMap::new();
for detected in &renames {
initial.insert(
Name::from(detected.rename.old.as_ref()),
Name::from(detected.rename.new.as_ref()),
);
}
let opts = SearchOptions {
initial,
..SearchOptions::default()
};
let best = find_best_morphism(old, new, &opts)?;
if best.vertex_map.len() > identity_mig.vertex_map.len() {
let mut hom_mig = morphism_to_migration(&best);
hom_mig
.hyper_edge_map
.clone_from(&identity_mig.hyper_edge_map);
hom_mig.label_map.clone_from(&identity_mig.label_map);
Some(hom_mig)
} else {
None
}
}
fn find_matching_edge(schema: &Schema, src: &str, tgt: &str, name: Option<&str>) -> Option<Edge> {
schema
.edges
.keys()
.find(|e| e.src == src && e.tgt == tgt && e.name.as_deref() == name)
.cloned()
}
fn find_label_for_vertex(he: &panproto_schema::HyperEdge, vertex_id: &str) -> Option<Name> {
he.signature
.iter()
.find(|(_, v)| **v == *vertex_id)
.map(|(label, _)| label.clone())
}
#[cfg(test)]
mod tests {
use super::*;
use panproto_check::diff::diff;
use panproto_schema::Vertex;
fn make_schema(vertices: &[(&str, &str)], edges: &[Edge]) -> Schema {
let mut vert_map = HashMap::new();
let mut edge_map = HashMap::new();
for (id, kind) in vertices {
vert_map.insert(
Name::from(*id),
Vertex {
id: Name::from(*id),
kind: Name::from(*kind),
nsid: None,
},
);
}
for edge in edges {
edge_map.insert(edge.clone(), edge.kind.clone());
}
Schema {
protocol: "test".into(),
vertices: vert_map,
edges: edge_map,
hyper_edges: HashMap::new(),
constraints: HashMap::new(),
required: HashMap::new(),
nsids: HashMap::new(),
entries: Vec::new(),
variants: HashMap::new(),
orderings: HashMap::new(),
recursion_points: HashMap::new(),
spans: HashMap::new(),
usage_modes: HashMap::new(),
nominal: HashMap::new(),
coercions: HashMap::new(),
mergers: HashMap::new(),
defaults: HashMap::new(),
policies: HashMap::new(),
outgoing: HashMap::new(),
incoming: HashMap::new(),
between: HashMap::new(),
}
}
#[test]
fn derive_identity_for_unchanged() {
let s = make_schema(&[("a", "object"), ("b", "string")], &[]);
let d = diff(&s, &s);
let m = derive_migration(&s, &s, &d);
assert_eq!(m.vertex_map.len(), 2);
assert_eq!(m.vertex_map["a"], "a");
assert_eq!(m.vertex_map["b"], "b");
}
#[test]
fn derive_drops_removed_vertices() {
let old = make_schema(&[("a", "object"), ("b", "string")], &[]);
let new = make_schema(&[("a", "object")], &[]);
let d = diff(&old, &new);
let m = derive_migration(&old, &new, &d);
assert_eq!(m.vertex_map.len(), 1);
assert!(m.vertex_map.contains_key("a"));
assert!(!m.vertex_map.contains_key("b"));
}
#[test]
fn derive_keeps_edges_with_surviving_endpoints() {
let edge = Edge {
src: "a".into(),
tgt: "b".into(),
kind: "prop".into(),
name: Some("x".into()),
};
let old = make_schema(
&[("a", "object"), ("b", "string")],
std::slice::from_ref(&edge),
);
let new = make_schema(
&[("a", "object"), ("b", "string")],
std::slice::from_ref(&edge),
);
let d = diff(&old, &new);
let m = derive_migration(&old, &new, &d);
assert_eq!(m.edge_map.len(), 1);
}
#[test]
fn derive_drops_edges_with_removed_endpoints() {
let edge = Edge {
src: "a".into(),
tgt: "b".into(),
kind: "prop".into(),
name: None,
};
let old = make_schema(&[("a", "object"), ("b", "string")], &[edge]);
let new = make_schema(&[("a", "object")], &[]);
let d = diff(&old, &new);
let m = derive_migration(&old, &new, &d);
assert!(m.edge_map.is_empty());
}
#[test]
fn derive_handles_addition() {
let old = make_schema(&[("a", "object")], &[]);
let new = make_schema(&[("a", "object"), ("b", "string")], &[]);
let d = diff(&old, &new);
let m = derive_migration(&old, &new, &d);
assert_eq!(m.vertex_map.len(), 1);
assert!(m.vertex_map.contains_key("a"));
}
}