use std::collections::HashMap;
use uni_common::Properties;
use uni_cypher::ast::Query;
use uni_cypher::locy_ast::{DeriveClause, DeriveCommand, DerivePattern, RuleOutput};
use uni_locy::result::DerivedEdge;
use uni_locy::{CompiledProgram, FactRow, LocyError, LocyStats};
use super::locy_ast_builder::build_derive_create;
use super::locy_eval::eval_expr;
use super::locy_traits::LocyExecutionContext;
pub struct CollectedDeriveOutput {
pub queries: Vec<Query>,
pub vertices: HashMap<String, Vec<Properties>>,
pub edges: Vec<DerivedEdge>,
pub affected: usize,
}
pub async fn derive_command(
dc: &DeriveCommand,
program: &CompiledProgram,
ctx: &dyn LocyExecutionContext,
stats: &mut LocyStats,
) -> Result<usize, LocyError> {
let collected = collect_derive_facts_inner(dc, program, ctx).await?;
for query in collected.queries {
ctx.execute_mutation(query, HashMap::new()).await?;
stats.mutations_executed += 1;
}
Ok(collected.affected)
}
pub async fn collect_derive_facts(
dc: &DeriveCommand,
program: &CompiledProgram,
ctx: &dyn LocyExecutionContext,
) -> Result<CollectedDeriveOutput, LocyError> {
collect_derive_facts_inner(dc, program, ctx).await
}
async fn collect_derive_facts_inner(
dc: &DeriveCommand,
program: &CompiledProgram,
ctx: &dyn LocyExecutionContext,
) -> Result<CollectedDeriveOutput, LocyError> {
let rule_name = dc.rule_name.to_string();
let rule = program
.rule_catalog
.get(&rule_name)
.ok_or_else(|| LocyError::EvaluationError {
message: format!("rule '{}' not found for DERIVE command", rule_name),
})?;
let facts = ctx.lookup_derived_enriched(&rule_name).await?;
let filtered: Vec<_> = if let Some(where_expr) = &dc.where_expr {
facts
.into_iter()
.filter(|row| {
eval_expr(where_expr, row)
.map(|v| v.as_bool().unwrap_or(false))
.unwrap_or(false)
})
.collect()
} else {
facts
};
let mut all_queries = Vec::new();
let mut all_vertices: HashMap<String, Vec<Properties>> = HashMap::new();
let mut all_edges = Vec::new();
let mut affected = 0;
for clause in &rule.clauses {
if let RuleOutput::Derive(derive_clause) = &clause.output {
for row in &filtered {
let queries = build_derive_create(derive_clause, row)?;
affected += queries.len();
extract_vertex_edge_data(derive_clause, row, &mut all_vertices, &mut all_edges);
all_queries.extend(queries);
}
}
}
Ok(CollectedDeriveOutput {
queries: all_queries,
vertices: all_vertices,
edges: all_edges,
affected,
})
}
fn extract_vertex_edge_data(
derive_clause: &DeriveClause,
row: &FactRow,
vertices: &mut HashMap<String, Vec<Properties>>,
edges: &mut Vec<DerivedEdge>,
) {
match derive_clause {
DeriveClause::Patterns(patterns) => {
for pattern in patterns {
extract_from_pattern(pattern, row, vertices, edges);
}
}
DeriveClause::Merge(a, b) => {
let source_props = node_properties_from_binding(a, row);
let target_props = node_properties_from_binding(b, row);
edges.push(DerivedEdge {
edge_type: "MERGED_WITH".to_string(),
source_label: node_label_from_binding(a, row),
source_properties: source_props,
target_label: node_label_from_binding(b, row),
target_properties: target_props,
edge_properties: Properties::new(),
});
}
}
}
fn extract_from_pattern(
pattern: &DerivePattern,
row: &FactRow,
vertices: &mut HashMap<String, Vec<Properties>>,
edges: &mut Vec<DerivedEdge>,
) {
let source = &pattern.source;
let target = &pattern.target;
let edge = &pattern.edge;
let source_label = source
.labels
.first()
.cloned()
.unwrap_or_else(|| node_label_from_binding(&source.variable, row));
let target_label = target
.labels
.first()
.cloned()
.unwrap_or_else(|| node_label_from_binding(&target.variable, row));
let source_props = node_properties_from_binding(&source.variable, row);
let target_props = node_properties_from_binding(&target.variable, row);
if source.is_new {
vertices
.entry(source_label.clone())
.or_default()
.push(source_props.clone());
}
if target.is_new {
vertices
.entry(target_label.clone())
.or_default()
.push(target_props.clone());
}
let edge_props = edge
.properties
.as_ref()
.and_then(|expr| eval_map_expr(expr, row))
.unwrap_or_default();
edges.push(DerivedEdge {
edge_type: edge.edge_type.clone(),
source_label,
source_properties: source_props,
target_label,
target_properties: target_props,
edge_properties: edge_props,
});
}
fn node_properties_from_binding(var: &str, row: &FactRow) -> Properties {
use uni_common::Value;
match row.get(var) {
Some(Value::Node(node)) => node.properties.clone(),
Some(Value::Map(map)) => map.clone(),
_ => Properties::new(),
}
}
fn node_label_from_binding(var: &str, row: &FactRow) -> String {
use uni_common::Value;
match row.get(var) {
Some(Value::Node(node)) => node.labels.first().cloned().unwrap_or_default(),
_ => String::new(),
}
}
fn eval_map_expr(expr: &uni_cypher::ast::Expr, row: &FactRow) -> Option<Properties> {
use uni_common::Value;
match eval_expr(expr, row) {
Ok(Value::Map(m)) => Some(m),
_ => None,
}
}