use std::collections::HashMap;
use petgraph::graph::NodeIndex;
use petgraph::Direction;
use super::super::ast::YieldItem;
use super::super::result::ResultRow;
use crate::datatypes::values::Value;
use crate::graph::dir_graph::DirGraph;
use crate::graph::schema::InternedKey;
use crate::graph::storage::GraphRead;
pub(super) fn execute_orphan_node(
graph: &DirGraph,
params: &HashMap<String, Value>,
yield_items: &[YieldItem],
) -> Result<Vec<ResultRow>, String> {
let node_type = require_string_param(params, "type", "orphan_node")?;
let link_type = optional_string_param(params, "link_type", "orphan_node")?;
let direction = optional_string_param(params, "direction", "orphan_node")?
.unwrap_or_else(|| "both".to_string());
let check_out = matches!(direction.as_str(), "out" | "both");
let check_in = matches!(direction.as_str(), "in" | "both");
if !check_out && !check_in {
return Err(format!(
"CALL orphan_node: invalid direction '{direction}'. Use 'in', 'out', or 'both'."
));
}
let edge_key = link_type.as_deref().map(InternedKey::from_str);
let yield_var = require_node_yield(yield_items, "orphan_node", "node")?;
let nodes = type_indices(graph, &node_type)?;
let mut rows = Vec::new();
for nidx in nodes.iter() {
let has_match = |dir: Direction| -> bool {
let mut iter = graph.graph.edges_directed(nidx, dir);
match edge_key {
Some(key) => iter.any(|er| er.weight().connection_type == key),
None => iter.next().is_some(),
}
};
if check_out && has_match(Direction::Outgoing) {
continue;
}
if check_in && has_match(Direction::Incoming) {
continue;
}
rows.push(make_node_row(&yield_var, nidx));
}
Ok(rows)
}
pub(super) fn execute_self_loop(
graph: &DirGraph,
params: &HashMap<String, Value>,
yield_items: &[YieldItem],
) -> Result<Vec<ResultRow>, String> {
let node_type = require_string_param(params, "type", "self_loop")?;
let edge_type = require_string_param(params, "edge", "self_loop")?;
let yield_var = require_node_yield(yield_items, "self_loop", "node")?;
let nodes = type_indices(graph, &node_type)?;
let edge_key = InternedKey::from_str(&edge_type);
let mut rows = Vec::new();
for nidx in nodes.iter() {
let hit = graph
.graph
.edges_directed(nidx, Direction::Outgoing)
.any(|er| er.target() == nidx && er.weight().connection_type == edge_key);
if hit {
rows.push(make_node_row(&yield_var, nidx));
}
}
Ok(rows)
}
pub(super) fn execute_cycle_2step(
graph: &DirGraph,
params: &HashMap<String, Value>,
yield_items: &[YieldItem],
) -> Result<Vec<ResultRow>, String> {
let node_type = require_string_param(params, "type", "cycle_2step")?;
let edge_type = require_string_param(params, "edge", "cycle_2step")?;
let (start_var, end_var) =
require_two_node_yields(yield_items, "cycle_2step", "node_a", "node_b")?;
let nodes = type_indices(graph, &node_type)?;
let node_set: std::collections::HashSet<NodeIndex> = nodes.iter().collect();
let edge_key = InternedKey::from_str(&edge_type);
let mut rows = Vec::new();
for a in nodes.iter() {
for er_a in graph.graph.edges_directed(a, Direction::Outgoing) {
if er_a.weight().connection_type != edge_key {
continue;
}
let b = er_a.target();
if a >= b {
continue;
}
if !node_set.contains(&b) {
continue;
}
let returns = graph
.graph
.edges_directed(b, Direction::Outgoing)
.any(|er_b| er_b.target() == a && er_b.weight().connection_type == edge_key);
if returns {
let mut row = ResultRow::new();
row.node_bindings.insert(start_var.clone(), a);
row.node_bindings.insert(end_var.clone(), b);
rows.push(row);
}
}
}
Ok(rows)
}
pub(super) fn execute_missing_required_edge(
graph: &DirGraph,
params: &HashMap<String, Value>,
yield_items: &[YieldItem],
) -> Result<Vec<ResultRow>, String> {
let node_type = require_string_param(params, "type", "missing_required_edge")?;
let edge_type = require_string_param(params, "edge", "missing_required_edge")?;
let yield_var = require_node_yield(yield_items, "missing_required_edge", "node")?;
validate_direction(
graph,
&node_type,
&edge_type,
OUTBOUND,
"missing_required_edge",
)?;
let nodes = type_indices(graph, &node_type)?;
let edge_key = InternedKey::from_str(&edge_type);
let mut rows = Vec::new();
for nidx in nodes.iter() {
let has_edge = has_edge_of_type(graph, nidx, Direction::Outgoing, &edge_type, edge_key);
if !has_edge {
rows.push(make_node_row(&yield_var, nidx));
}
}
Ok(rows)
}
pub(super) fn execute_missing_inbound_edge(
graph: &DirGraph,
params: &HashMap<String, Value>,
yield_items: &[YieldItem],
) -> Result<Vec<ResultRow>, String> {
let node_type = require_string_param(params, "type", "missing_inbound_edge")?;
let edge_type = require_string_param(params, "edge", "missing_inbound_edge")?;
let yield_var = require_node_yield(yield_items, "missing_inbound_edge", "node")?;
validate_direction(
graph,
&node_type,
&edge_type,
INBOUND,
"missing_inbound_edge",
)?;
let nodes = type_indices(graph, &node_type)?;
let edge_key = InternedKey::from_str(&edge_type);
let mut rows = Vec::new();
for nidx in nodes.iter() {
let has_edge = has_edge_of_type(graph, nidx, Direction::Incoming, &edge_type, edge_key);
if !has_edge {
rows.push(make_node_row(&yield_var, nidx));
}
}
Ok(rows)
}
pub(super) fn execute_duplicate_title(
graph: &DirGraph,
params: &HashMap<String, Value>,
yield_items: &[YieldItem],
) -> Result<Vec<ResultRow>, String> {
let node_type = require_string_param(params, "type", "duplicate_title")?;
let yield_var = require_node_yield(yield_items, "duplicate_title", "node")?;
let nodes = type_indices(graph, &node_type)?;
let mut counts: HashMap<String, u32> = HashMap::with_capacity(nodes.len());
for nidx in nodes.iter() {
if let Some(title) = title_of(graph, nidx) {
*counts.entry(title).or_insert(0) += 1;
}
}
let mut rows = Vec::new();
for nidx in nodes.iter() {
if let Some(title) = title_of(graph, nidx) {
if counts.get(&title).copied().unwrap_or(0) > 1 {
rows.push(make_node_row(&yield_var, nidx));
}
}
}
Ok(rows)
}
pub(super) fn execute_inverse_violation(
graph: &DirGraph,
params: &HashMap<String, Value>,
yield_items: &[YieldItem],
) -> Result<Vec<ResultRow>, String> {
let rel_a = require_string_param(params, "rel_a", "inverse_violation")?;
let rel_b = require_string_param(params, "rel_b", "inverse_violation")?;
let (a_var, b_var) = require_two_node_yields(yield_items, "inverse_violation", "a", "b")?;
let key_a = InternedKey::from_str(&rel_a);
let key_b = InternedKey::from_str(&rel_b);
let mut rows = Vec::new();
for a in graph.graph.node_indices() {
for er in graph.graph.edges_directed(a, Direction::Outgoing) {
if er.weight().connection_type != key_a {
continue;
}
let b = er.target();
let has_inverse = graph
.graph
.edges_directed(b, Direction::Outgoing)
.any(|er2| er2.target() == a && er2.weight().connection_type == key_b);
if !has_inverse {
let mut row = ResultRow::new();
row.node_bindings.insert(a_var.clone(), a);
row.node_bindings.insert(b_var.clone(), b);
rows.push(row);
}
}
}
Ok(rows)
}
pub(super) fn execute_transitivity_violation(
graph: &DirGraph,
params: &HashMap<String, Value>,
yield_items: &[YieldItem],
) -> Result<Vec<ResultRow>, String> {
let rel = require_string_param(params, "rel", "transitivity_violation")?;
let a_var = require_node_yield(yield_items, "transitivity_violation", "a")?;
let b_var = require_node_yield(yield_items, "transitivity_violation", "b")?;
let c_var = require_node_yield(yield_items, "transitivity_violation", "c")?;
let key = InternedKey::from_str(&rel);
let mut rows = Vec::new();
for a in graph.graph.node_indices() {
let direct_a: std::collections::HashSet<NodeIndex> = graph
.graph
.edges_directed(a, Direction::Outgoing)
.filter(|er| er.weight().connection_type == key)
.map(|er| er.target())
.collect();
for &b in &direct_a {
for er_b in graph.graph.edges_directed(b, Direction::Outgoing) {
if er_b.weight().connection_type != key {
continue;
}
let c = er_b.target();
if c == a || c == b {
continue;
}
if !direct_a.contains(&c) {
let mut row = ResultRow::new();
row.node_bindings.insert(a_var.clone(), a);
row.node_bindings.insert(b_var.clone(), b);
row.node_bindings.insert(c_var.clone(), c);
rows.push(row);
}
}
}
}
Ok(rows)
}
pub(super) fn execute_cardinality_violation(
graph: &DirGraph,
params: &HashMap<String, Value>,
yield_items: &[YieldItem],
) -> Result<Vec<ResultRow>, String> {
let node_type = require_string_param(params, "type", "cardinality_violation")?;
let edge_type = require_string_param(params, "edge", "cardinality_violation")?;
let min_count = call_param_i64(params, "min", 0).max(0) as usize;
let max_count = call_param_opt_i64(params, "max").map(|v| v.max(0) as usize);
let node_var = require_node_yield(yield_items, "cardinality_violation", "node")?;
let count_var = require_scalar_yield(yield_items, "cardinality_violation", "count")?;
let nodes = type_indices(graph, &node_type)?;
let edge_key = InternedKey::from_str(&edge_type);
let mut rows = Vec::new();
for nidx in nodes.iter() {
let count = graph
.graph
.edges_directed(nidx, Direction::Outgoing)
.filter(|er| er.weight().connection_type == edge_key)
.count();
let too_few = count < min_count;
let too_many = max_count.is_some_and(|m| count > m);
if too_few || too_many {
let mut row = ResultRow::new();
row.node_bindings.insert(node_var.clone(), nidx);
row.projected
.insert(count_var.clone(), Value::Int64(count as i64));
rows.push(row);
}
}
Ok(rows)
}
pub(super) fn execute_type_domain_violation(
graph: &DirGraph,
params: &HashMap<String, Value>,
yield_items: &[YieldItem],
) -> Result<Vec<ResultRow>, String> {
let edge_type = require_string_param(params, "edge", "type_domain_violation")?;
let expected = require_string_param(params, "expected_source", "type_domain_violation")?;
let src_var = require_node_yield(yield_items, "type_domain_violation", "source")?;
let tgt_var = require_node_yield(yield_items, "type_domain_violation", "target")?;
let key = InternedKey::from_str(&edge_type);
let mut rows = Vec::new();
for er in graph.graph.edge_references() {
if er.weight().connection_type != key {
continue;
}
let src = er.source();
let actual_type = match graph.graph.node_weight(src) {
Some(n) => n.node_type_str(&graph.interner),
None => continue,
};
if actual_type != expected {
let mut row = ResultRow::new();
row.node_bindings.insert(src_var.clone(), src);
row.node_bindings.insert(tgt_var.clone(), er.target());
rows.push(row);
}
}
Ok(rows)
}
pub(super) fn execute_type_range_violation(
graph: &DirGraph,
params: &HashMap<String, Value>,
yield_items: &[YieldItem],
) -> Result<Vec<ResultRow>, String> {
let edge_type = require_string_param(params, "edge", "type_range_violation")?;
let expected = require_string_param(params, "expected_target", "type_range_violation")?;
let src_var = require_node_yield(yield_items, "type_range_violation", "source")?;
let tgt_var = require_node_yield(yield_items, "type_range_violation", "target")?;
let key = InternedKey::from_str(&edge_type);
let mut rows = Vec::new();
for er in graph.graph.edge_references() {
if er.weight().connection_type != key {
continue;
}
let tgt = er.target();
let actual_type = match graph.graph.node_weight(tgt) {
Some(n) => n.node_type_str(&graph.interner),
None => continue,
};
if actual_type != expected {
let mut row = ResultRow::new();
row.node_bindings.insert(src_var.clone(), er.source());
row.node_bindings.insert(tgt_var.clone(), tgt);
rows.push(row);
}
}
Ok(rows)
}
pub(super) fn execute_parallel_edges(
graph: &DirGraph,
params: &HashMap<String, Value>,
yield_items: &[YieldItem],
) -> Result<Vec<ResultRow>, String> {
let edge_type = require_string_param(params, "edge", "parallel_edges")?;
let (a_var, b_var) = require_two_node_yields(yield_items, "parallel_edges", "a", "b")?;
let count_var = require_scalar_yield(yield_items, "parallel_edges", "count")?;
let key = InternedKey::from_str(&edge_type);
let mut counts: HashMap<(NodeIndex, NodeIndex), u32> = HashMap::new();
for er in graph.graph.edge_references() {
if er.weight().connection_type == key {
*counts.entry((er.source(), er.target())).or_insert(0) += 1;
}
}
let mut rows = Vec::new();
for ((a, b), c) in counts {
if c > 1 {
let mut row = ResultRow::new();
row.node_bindings.insert(a_var.clone(), a);
row.node_bindings.insert(b_var.clone(), b);
row.projected
.insert(count_var.clone(), Value::Int64(c as i64));
rows.push(row);
}
}
Ok(rows)
}
pub(super) fn execute_null_property(
graph: &DirGraph,
params: &HashMap<String, Value>,
yield_items: &[YieldItem],
) -> Result<Vec<ResultRow>, String> {
let node_type = require_string_param(params, "type", "null_property")?;
let property = require_string_param(params, "property", "null_property")?;
let yield_var = require_node_yield(yield_items, "null_property", "node")?;
let nodes = type_indices(graph, &node_type)?;
let mut rows = Vec::new();
for nidx in nodes.iter() {
let node = match graph.graph.node_weight(nidx) {
Some(n) => n,
None => continue,
};
let val = node.get_property_value(&property);
let is_null = match val {
None => true,
Some(Value::Null) => true,
Some(Value::String(ref s)) if s.is_empty() => true,
_ => false,
};
if is_null {
rows.push(make_node_row(&yield_var, nidx));
}
}
Ok(rows)
}
pub(super) fn execute_kg_knn(
graph: &DirGraph,
params: &HashMap<String, Value>,
yield_items: &[YieldItem],
) -> Result<Vec<ResultRow>, String> {
let lat = call_param_f64_required(params, "lat", "kg_knn")?;
let lon = call_param_f64_required(params, "lon", "kg_knn")?;
let target_type = require_string_param(params, "target_type", "kg_knn")?;
let k = call_param_i64(params, "k", 10).max(1) as usize;
let node_var = require_node_yield(yield_items, "kg_knn", "node")?;
let dist_var = require_scalar_yield(yield_items, "kg_knn", "distance_m")?;
let nodes = type_indices(graph, &target_type)?;
use std::cmp::Ordering;
use std::collections::BinaryHeap;
#[derive(PartialEq)]
struct Entry(f64, NodeIndex);
impl Eq for Entry {}
impl Ord for Entry {
fn cmp(&self, other: &Self) -> Ordering {
other
.0
.partial_cmp(&self.0)
.unwrap_or(Ordering::Equal)
.then_with(|| self.1.index().cmp(&other.1.index()))
}
}
impl PartialOrd for Entry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
let mut heap: BinaryHeap<Entry> = BinaryHeap::with_capacity(nodes.len());
let config = graph.get_spatial_config(&target_type);
for nidx in nodes.iter() {
let node = match graph.graph.node_weight(nidx) {
Some(n) => n,
None => continue,
};
let mut dist = f64::INFINITY;
if let Some(cfg) = config {
if let Some((lat_f, lon_f)) = &cfg.location {
if let (Some(plat), Some(plon)) = (
node.get_property(lat_f)
.as_deref()
.and_then(crate::graph::core::value_operations::value_to_f64),
node.get_property(lon_f)
.as_deref()
.and_then(crate::graph::core::value_operations::value_to_f64),
) {
dist = crate::graph::features::spatial::geodesic_distance(lat, lon, plat, plon);
}
}
if dist == f64::INFINITY {
if let Some(geom_f) = &cfg.geometry {
if let Some(Value::String(wkt)) = node.get_property(geom_f).as_deref() {
if let Ok(g) = crate::graph::features::spatial::parse_wkt(wkt) {
if let Ok((clat, clon)) =
crate::graph::features::spatial::wkt_centroid(wkt)
{
dist = crate::graph::features::spatial::geodesic_distance(
lat, lon, clat, clon,
);
}
let _ = g; }
}
}
}
}
if dist.is_finite() {
heap.push(Entry(dist, nidx));
}
}
let mut rows: Vec<ResultRow> = Vec::with_capacity(k);
while rows.len() < k {
match heap.pop() {
Some(Entry(d, idx)) => {
let mut row = ResultRow::new();
row.node_bindings.insert(node_var.clone(), idx);
row.projected.insert(dist_var.clone(), Value::Float64(d));
rows.push(row);
}
None => break,
}
}
Ok(rows)
}
fn call_param_f64_required(
params: &HashMap<String, Value>,
key: &str,
proc: &str,
) -> Result<f64, String> {
match params.get(key) {
Some(Value::Int64(v)) => Ok(*v as f64),
Some(Value::Float64(v)) => Ok(*v),
Some(other) => Err(format!(
"CALL {proc}: parameter '{key}' must be numeric, got {other:?}"
)),
None => Err(format!(
"CALL {proc}: missing required parameter '{key}'.{}",
param_schema_hint(proc)
)),
}
}
const RULE_PARAM_SCHEMAS: &[(&str, &[(&str, bool)])] = &[
(
"cardinality_violation",
&[
("type", true),
("edge", true),
("min", false),
("max", false),
],
),
("cycle_2step", &[("type", true), ("edge", true)]),
("duplicate_title", &[("type", true)]),
("inverse_violation", &[("rel_a", true), ("rel_b", true)]),
(
"kg_knn",
&[
("lat", true),
("lon", true),
("target_type", true),
("k", false),
],
),
("missing_inbound_edge", &[("type", true), ("edge", true)]),
("missing_required_edge", &[("type", true), ("edge", true)]),
("null_property", &[("type", true), ("property", true)]),
(
"orphan_node",
&[("type", true), ("link_type", false), ("direction", false)],
),
("parallel_edges", &[("edge", true)]),
("self_loop", &[("type", true), ("edge", true)]),
("transitivity_violation", &[("rel", true)]),
(
"type_domain_violation",
&[("edge", true), ("expected_source", true)],
),
(
"type_range_violation",
&[("edge", true), ("expected_target", true)],
),
];
fn param_schema_hint(proc: &str) -> String {
let Some((_, schema)) = RULE_PARAM_SCHEMAS.iter().find(|(name, _)| *name == proc) else {
return String::new();
};
let required: Vec<&str> = schema
.iter()
.filter_map(|(name, req)| if *req { Some(*name) } else { None })
.collect();
let optional: Vec<&str> = schema
.iter()
.filter_map(|(name, req)| if !*req { Some(*name) } else { None })
.collect();
let mut parts = Vec::new();
if !required.is_empty() {
parts.push(format!("required: [{}]", required.join(", ")));
}
if !optional.is_empty() {
parts.push(format!("optional: [{}]", optional.join(", ")));
}
if parts.is_empty() {
String::new()
} else {
format!(" Accepted parameters — {}.", parts.join(", "))
}
}
fn has_edge_of_type(
graph: &DirGraph,
nidx: NodeIndex,
dir: Direction,
_edge_type_name: &str,
edge_type_key: InternedKey,
) -> bool {
graph
.graph
.edges_directed(nidx, dir)
.any(|er| er.weight().connection_type == edge_type_key)
}
fn title_of(graph: &DirGraph, nidx: NodeIndex) -> Option<String> {
let nd = graph.graph.node_weight(nidx)?;
match nd.title().as_ref() {
Value::String(s) => Some(s.clone()),
Value::Null => None,
other => Some(format!("{other:?}")),
}
}
const OUTBOUND: bool = true;
const INBOUND: bool = false;
fn require_string_param(
params: &HashMap<String, Value>,
key: &str,
proc: &str,
) -> Result<String, String> {
match params.get(key) {
Some(Value::String(s)) => Ok(s.clone()),
Some(other) => Err(format!(
"CALL {proc}: parameter '{key}' must be a string, got {other:?}"
)),
None => Err(format!(
"CALL {proc}: missing required parameter '{key}'. \
Use map syntax — e.g. CALL {proc}({{{key}: 'X'}}).{}",
param_schema_hint(proc)
)),
}
}
fn optional_string_param(
params: &HashMap<String, Value>,
key: &str,
proc: &str,
) -> Result<Option<String>, String> {
match params.get(key) {
Some(Value::String(s)) => Ok(Some(s.clone())),
Some(other) => Err(format!(
"CALL {proc}: parameter '{key}' must be a string, got {other:?}"
)),
None => Ok(None),
}
}
fn require_node_yield(
yield_items: &[YieldItem],
proc: &str,
expected: &str,
) -> Result<String, String> {
if let Some(item) = yield_items.iter().find(|y| y.name == expected) {
Ok(item.alias.clone().unwrap_or_else(|| expected.to_string()))
} else {
Err(format!(
"CALL {proc}: must YIELD '{expected}'. Got {got:?}.",
got = yield_items
.iter()
.map(|y| y.name.as_str())
.collect::<Vec<_>>(),
))
}
}
fn require_two_node_yields(
yield_items: &[YieldItem],
proc: &str,
a: &str,
b: &str,
) -> Result<(String, String), String> {
let av = require_node_yield(yield_items, proc, a)?;
let bv = require_node_yield(yield_items, proc, b)?;
Ok((av, bv))
}
fn require_scalar_yield(
yield_items: &[YieldItem],
proc: &str,
expected: &str,
) -> Result<String, String> {
require_node_yield(yield_items, proc, expected)
}
fn call_param_i64(params: &HashMap<String, Value>, key: &str, default: i64) -> i64 {
match params.get(key) {
Some(Value::Int64(v)) => *v,
Some(Value::Float64(v)) => *v as i64,
_ => default,
}
}
fn call_param_opt_i64(params: &HashMap<String, Value>, key: &str) -> Option<i64> {
match params.get(key) {
Some(Value::Int64(v)) => Some(*v),
Some(Value::Float64(v)) => Some(*v as i64),
_ => None,
}
}
fn type_indices<'a>(
graph: &'a DirGraph,
node_type: &str,
) -> Result<crate::graph::storage::disk::type_index::TypeNodesRef<'a>, String> {
graph
.type_indices
.get(node_type)
.ok_or_else(|| format!("Type '{node_type}' has no nodes in this graph"))
}
fn make_node_row(yield_var: &str, nidx: NodeIndex) -> ResultRow {
let mut row = ResultRow::new();
row.node_bindings.insert(yield_var.to_string(), nidx);
row
}
fn validate_direction(
graph: &DirGraph,
node_type: &str,
edge_type: &str,
outbound: bool,
proc: &str,
) -> Result<(), String> {
let info = match graph.connection_type_metadata.get(edge_type) {
Some(i) => i,
None => return Ok(()), };
let on_correct_side = if outbound {
info.source_types.contains(node_type)
} else {
info.target_types.contains(node_type)
};
if on_correct_side || info.source_types.is_empty() || info.target_types.is_empty() {
return Ok(());
}
let mut sources: Vec<&String> = info.source_types.iter().collect();
let mut targets: Vec<&String> = info.target_types.iter().collect();
sources.sort();
targets.sort();
if outbound {
if info.target_types.contains(node_type) {
Err(format!(
"DirectionMismatch in CALL {proc}: '{edge_type}' flows {sources:?} → \
{targets:?} — '{node_type}' is on the target side. \
Use missing_inbound_edge with the same parameters instead."
))
} else {
Err(format!(
"DirectionMismatch in CALL {proc}: '{edge_type}' flows {sources:?} → \
{targets:?} — '{node_type}' isn't a source type for this edge. \
Set type to one of {sources:?}, or pick a different edge."
))
}
} else if info.source_types.contains(node_type) {
Err(format!(
"DirectionMismatch in CALL {proc}: '{edge_type}' flows {sources:?} → \
{targets:?} — '{node_type}' is on the source side. \
Use missing_required_edge with the same parameters instead."
))
} else {
Err(format!(
"DirectionMismatch in CALL {proc}: '{edge_type}' flows {sources:?} → \
{targets:?} — '{node_type}' isn't a target type for this edge. \
Set type to one of {targets:?}, or pick a different edge."
))
}
}