use std::collections::{HashMap, HashSet};
use crate::generated::model::EntityKey;
use crate::generated::profile::SchemaProfile;
use crate::generated::write::Writer;
#[doc(hidden)]
#[derive(Clone, Debug, Default)]
pub struct LossReport {
pub dropped: Vec<(String, &'static str)>,
pub downgraded: Vec<(String, &'static str)>,
}
impl LossReport {
#[must_use]
pub fn is_empty(&self) -> bool {
self.dropped.is_empty() && self.downgraded.is_empty()
}
}
pub(crate) struct ProjectionPlan {
pub dropped: HashSet<EntityKey>,
pub rename: HashMap<EntityKey, &'static str>,
pub loss: LossReport,
}
const R_ILLEGAL: &str = "illegal in target schema";
const R_CASCADE: &str = "cascade (references a dropped entity)";
const R_ORPHAN: &str = "unreachable after projection";
fn display_name(any: EntityKey) -> String {
match any {
EntityKey::ComplexUnit(_) => "(complex)".to_string(),
_ => Writer::name_of(any).to_string(),
}
}
fn deps(w: &Writer, a: EntityKey) -> Vec<EntityKey> {
let mut v = Vec::new();
w.deps_of(a, &mut v);
v.into_iter()
.filter(|&d| d != a)
.collect::<HashSet<_>>()
.into_iter()
.collect()
}
fn reach(
roots: &[EntityKey],
fwd: &HashMap<EntityKey, Vec<EntityKey>>,
allow: impl Fn(EntityKey) -> bool,
) -> HashSet<EntityKey> {
let mut seen: HashSet<EntityKey> = HashSet::new();
let mut stack: Vec<EntityKey> = Vec::new();
for &r in roots {
if allow(r) && seen.insert(r) {
stack.push(r);
}
}
while let Some(n) = stack.pop() {
if let Some(ds) = fwd.get(&n) {
for &d in ds {
if allow(d) && seen.insert(d) {
stack.push(d);
}
}
}
}
seen
}
pub(crate) fn plan(w: &Writer, profile: &SchemaProfile) -> ProjectionPlan {
let all = w.all_ids();
let mut fwd: HashMap<EntityKey, Vec<EntityKey>> = HashMap::with_capacity(all.len());
let mut rev: HashMap<EntityKey, Vec<EntityKey>> = HashMap::new();
let mut indeg: HashMap<EntityKey, usize> = HashMap::with_capacity(all.len());
for &a in &all {
indeg.entry(a).or_insert(0);
}
for &a in &all {
let ds = deps(w, a);
for &d in &ds {
rev.entry(d).or_default().push(a);
*indeg.entry(d).or_insert(0) += 1;
}
fwd.insert(a, ds);
}
let mut dropped: HashSet<EntityKey> = HashSet::new();
let mut rename: HashMap<EntityKey, &'static str> = HashMap::new();
let mut loss = LossReport::default();
for &a in &all {
if let EntityKey::ComplexUnit(id) = a {
if !w.complex_legal(id, profile.legal) {
dropped.insert(a);
loss.dropped.push((display_name(a), R_ILLEGAL));
}
} else {
let kw = Writer::name_of(a);
if !profile.is_legal(kw) {
if let Some(sup) = profile.downgrade(kw) {
rename.insert(a, sup);
loss.downgraded.push((kw.to_string(), sup));
} else {
dropped.insert(a);
loss.dropped.push((kw.to_string(), R_ILLEGAL));
}
}
}
}
let roots: Vec<EntityKey> = all
.iter()
.copied()
.filter(|a| indeg.get(a).copied().unwrap_or(0) == 0)
.collect();
let r_pre = reach(&roots, &fwd, |_| true);
let mut work: Vec<EntityKey> = dropped.iter().copied().collect();
while let Some(d) = work.pop() {
if let Some(referrers) = rev.get(&d) {
for &r in referrers {
if dropped.insert(r) {
loss.dropped.push((display_name(r), R_CASCADE));
work.push(r);
}
}
}
}
let live_roots: Vec<EntityKey> = roots
.iter()
.copied()
.filter(|a| !dropped.contains(a))
.collect();
let r_post = reach(&live_roots, &fwd, |a| !dropped.contains(&a));
for &a in &all {
if r_pre.contains(&a) && !r_post.contains(&a) && !dropped.contains(&a) {
dropped.insert(a);
loss.dropped.push((display_name(a), R_ORPHAN));
}
}
debug_assert!(
all.iter().filter(|a| !dropped.contains(a)).all(|a| fwd
.get(a)
.is_none_or(|ds| ds.iter().all(|d| !dropped.contains(d)))),
"projection left a survivor referencing a dropped entity"
);
ProjectionPlan {
dropped,
rename,
loss,
}
}