use crate::datatypes::{DataFrame, Value};
use crate::graph::schema::DirGraph;
use crate::graph::storage::lookups::CombinedTypeLookup;
use petgraph::graph::NodeIndex;
use std::collections::HashSet;
pub(super) struct ResolvedEndpoints {
pub(super) matched: Vec<(usize, NodeIndex, NodeIndex)>,
pub(super) deferred: Vec<(usize, Value, Value)>,
pub(super) missing_sources: Vec<Value>,
pub(super) missing_targets: Vec<Value>,
pub(super) null_source_rows: usize,
pub(super) null_target_rows: usize,
}
pub(super) fn resolve_endpoints(
graph: &DirGraph,
df_data: &DataFrame,
source_type: &str,
target_type: &str,
source_id_idx: usize,
target_id_idx: usize,
) -> Result<ResolvedEndpoints, String> {
if let Some(resolved) =
graph
.id_indices
.with_overlay_type_pair(source_type, target_type, |source, target| {
scan_endpoint_rows(
df_data,
source_id_idx,
target_id_idx,
|id| source.get(id),
|id| target.get(id),
)
})
{
return Ok(resolved);
}
let lookup = CombinedTypeLookup::from_id_indices(
&graph.id_indices,
&graph.graph,
source_type.to_string(),
target_type.to_string(),
)?;
Ok(scan_endpoint_rows(
df_data,
source_id_idx,
target_id_idx,
|id| lookup.check_source(id),
|id| lookup.check_target(id),
))
}
fn scan_endpoint_rows(
df_data: &DataFrame,
source_id_idx: usize,
target_id_idx: usize,
check_source: impl Fn(&Value) -> Option<NodeIndex>,
check_target: impl Fn(&Value) -> Option<NodeIndex>,
) -> ResolvedEndpoints {
let mut out = ResolvedEndpoints {
matched: Vec::with_capacity(df_data.row_count()),
deferred: Vec::new(),
missing_sources: Vec::new(),
missing_targets: Vec::new(),
null_source_rows: 0,
null_target_rows: 0,
};
let mut seen_missing_source: HashSet<Value> = HashSet::new();
let mut seen_missing_target: HashSet<Value> = HashSet::new();
for row_idx in 0..df_data.row_count() {
let source_id = match df_data.get_value_by_index(row_idx, source_id_idx) {
Some(Value::Null) | None => {
out.null_source_rows += 1;
continue;
}
Some(id) => id,
};
let target_id = match df_data.get_value_by_index(row_idx, target_id_idx) {
Some(Value::Null) | None => {
out.null_target_rows += 1;
continue;
}
Some(id) => id,
};
match (check_source(&source_id), check_target(&target_id)) {
(Some(source_idx), Some(target_idx)) => {
out.matched.push((row_idx, source_idx, target_idx))
}
(s_opt, t_opt) => {
if s_opt.is_none() && seen_missing_source.insert(source_id.clone()) {
out.missing_sources.push(source_id.clone());
}
if t_opt.is_none() && seen_missing_target.insert(target_id.clone()) {
out.missing_targets.push(target_id.clone());
}
out.deferred.push((row_idx, source_id, target_id));
}
}
}
out
}
pub(super) fn resolve_pairs(
graph: &DirGraph,
source_type: &str,
target_type: &str,
rows: &[(usize, Value, Value)],
) -> Result<Vec<Option<(NodeIndex, NodeIndex)>>, String> {
fn walk(
rows: &[(usize, Value, Value)],
check_source: impl Fn(&Value) -> Option<NodeIndex>,
check_target: impl Fn(&Value) -> Option<NodeIndex>,
) -> Vec<Option<(NodeIndex, NodeIndex)>> {
rows.iter()
.map(|(_, source_id, target_id)| {
match (check_source(source_id), check_target(target_id)) {
(Some(s), Some(t)) => Some((s, t)),
_ => None,
}
})
.collect()
}
if let Some(resolved) =
graph
.id_indices
.with_overlay_type_pair(source_type, target_type, |source, target| {
walk(rows, |id| source.get(id), |id| target.get(id))
})
{
return Ok(resolved);
}
let lookup = CombinedTypeLookup::from_id_indices(
&graph.id_indices,
&graph.graph,
source_type.to_string(),
target_type.to_string(),
)?;
Ok(walk(
rows,
|id| lookup.check_source(id),
|id| lookup.check_target(id),
))
}
pub(super) fn title_column_indices(
df_data: &DataFrame,
source_title_field: Option<&str>,
target_title_field: Option<&str>,
) -> (Option<usize>, Option<usize>) {
(
source_title_field.and_then(|field| df_data.get_column_index(field)),
target_title_field.and_then(|field| df_data.get_column_index(field)),
)
}
pub(super) fn report_null_id_skips(
errors: &mut Vec<String>,
source: (usize, &str),
target: (usize, &str),
) {
for (skipped, field, side) in [
(source.0, source.1, "source"),
(target.0, target.1, "target"),
] {
if skipped > 0 {
errors.push(format!(
"Skipped {skipped} rows: null values in {side} ID field '{field}'"
));
}
}
}