use super::super::ast::*;
use super::super::result::*;
use crate::datatypes::values::Value;
use crate::datatypes::{PropKey, PropMap};
use crate::graph::schema::{soft_alias_fallback, DirGraph, InternedKey, SoftAliasFallback};
use crate::graph::storage::{GraphRead, NodeView};
use std::collections::{HashMap, HashSet};
pub use super::super::ast::is_aggregate_expression;
pub(super) fn is_user_input_error(message: &str) -> bool {
super::regex_cache::is_compile_error(message)
|| super::expression::is_missing_parameter_error(message)
}
pub(crate) fn grouping_variables(items: &[ReturnItem]) -> HashSet<String> {
use crate::graph::languages::cypher::planner::simplification::collect_expression_refs;
let mut vars = HashSet::new();
for item in items {
if !is_aggregate_expression(&item.expression) {
collect_expression_refs(&item.expression, &mut vars);
}
}
vars
}
pub(crate) fn carry_group_bindings(
vars: &HashSet<String>,
source: &ResultRow,
target: &mut ResultRow,
) {
for var in vars {
if let Some(&idx) = source.node_bindings.get(var) {
target.node_bindings.insert(var.clone(), idx);
}
if let Some(edge) = source.edge_bindings.get(var) {
target.edge_bindings.insert(var.clone(), *edge);
}
if let Some(path) = source.path_bindings.get(var) {
target.path_bindings.insert(var.clone(), path.clone());
}
}
}
pub(super) fn augment_rows_with_aggregate_keys(rows: &mut [ResultRow], items: &[ReturnItem]) {
for item in items {
if !is_aggregate_expression(&item.expression) {
continue;
}
let alias_key = return_item_column_name(item);
let expr_key = expression_to_string(&item.expression);
if alias_key == expr_key {
continue;
}
for row in rows.iter_mut() {
if row.projected.contains_key(&expr_key) {
continue;
}
if let Some(val) = row.projected.get(&alias_key).cloned() {
row.projected.insert(expr_key.clone(), val);
}
}
}
}
pub fn return_item_column_name(item: &ReturnItem) -> String {
if let Some(ref alias) = item.alias {
alias.clone()
} else {
expression_to_string(&item.expression)
}
}
pub(crate) fn expression_to_string(expr: &Expression) -> String {
match expr {
Expression::PropertyAccess { variable, property } => format!("{}.{}", variable, property),
Expression::Variable(name) => name.clone(),
Expression::Literal(val) => format_value_compact(val),
Expression::FunctionCall {
name,
args,
distinct,
} => {
let args_str: Vec<String> = args.iter().map(expression_to_string).collect();
if *distinct {
format!("{}(DISTINCT {})", name, args_str.join(", "))
} else {
format!("{}({})", name, args_str.join(", "))
}
}
Expression::Star => "*".to_string(),
Expression::Add(l, r) => {
format!("{} + {}", expression_to_string(l), expression_to_string(r))
}
Expression::Subtract(l, r) => {
format!("{} - {}", expression_to_string(l), expression_to_string(r))
}
Expression::Multiply(l, r) => {
format!("{} * {}", expression_to_string(l), expression_to_string(r))
}
Expression::Divide(l, r) => {
format!("{} / {}", expression_to_string(l), expression_to_string(r))
}
Expression::Modulo(l, r) => {
format!("{} % {}", expression_to_string(l), expression_to_string(r))
}
Expression::Concat(l, r) => {
format!("{} || {}", expression_to_string(l), expression_to_string(r))
}
Expression::Negate(inner) => format!("-{}", expression_to_string(inner)),
Expression::ListLiteral(items) => {
let items_str: Vec<String> = items.iter().map(expression_to_string).collect();
format!("[{}]", items_str.join(", "))
}
Expression::Case { .. } => "CASE".to_string(),
Expression::Parameter(name) => format!("${}", name),
Expression::ListComprehension {
variable,
list_expr,
filter,
map_expr,
} => {
let mut result = format!("[{} IN {}", variable, expression_to_string(list_expr));
if filter.is_some() {
result.push_str(" WHERE ...");
}
if let Some(ref expr) = map_expr {
result.push_str(&format!(" | {}", expression_to_string(expr)));
}
result.push(']');
result
}
Expression::IndexAccess { expr, index } => {
format!(
"{}[{}]",
expression_to_string(expr),
expression_to_string(index)
)
}
Expression::ListSlice { expr, start, end } => {
let s = start
.as_ref()
.map_or(String::new(), |e| expression_to_string(e));
let e = end
.as_ref()
.map_or(String::new(), |e| expression_to_string(e));
format!("{}[{}..{}]", expression_to_string(expr), s, e)
}
Expression::MapProjection { variable, items } => {
let items_str: Vec<String> = items
.iter()
.map(|item| match item {
MapProjectionItem::Property(prop) => format!(".{}", prop),
MapProjectionItem::AllProperties => ".*".to_string(),
MapProjectionItem::Alias { key, expr } => {
format!("{}: {}", key, expression_to_string(expr))
}
})
.collect();
format!("{} {{{}}}", variable, items_str.join(", "))
}
Expression::MapLiteral(entries) => {
let items_str: Vec<String> = entries
.iter()
.map(|(key, expr)| format!("{}: {}", key, expression_to_string(expr)))
.collect();
format!("{{{}}}", items_str.join(", "))
}
Expression::IsNull(inner) => format!("{} IS NULL", expression_to_string(inner)),
Expression::IsNotNull(inner) => format!("{} IS NOT NULL", expression_to_string(inner)),
Expression::QuantifiedList {
quantifier,
variable,
list_expr,
..
} => {
let qname = match quantifier {
ListQuantifier::Any => "any",
ListQuantifier::All => "all",
ListQuantifier::None => "none",
ListQuantifier::Single => "single",
};
format!(
"{}({} IN {} WHERE ...)",
qname,
variable,
expression_to_string(list_expr)
)
}
Expression::WindowFunction {
name,
partition_by,
order_by,
} => {
let mut s = format!("{}() OVER (", name);
if !partition_by.is_empty() {
s.push_str("PARTITION BY ");
let parts: Vec<String> = partition_by.iter().map(expression_to_string).collect();
s.push_str(&parts.join(", "));
if !order_by.is_empty() {
s.push(' ');
}
}
if !order_by.is_empty() {
s.push_str("ORDER BY ");
let parts: Vec<String> = order_by
.iter()
.map(|item| {
let dir = if item.ascending { "" } else { " DESC" };
format!("{}{}", expression_to_string(&item.expression), dir)
})
.collect();
s.push_str(&parts.join(", "));
}
s.push(')');
s
}
Expression::PredicateExpr(pred) => predicate_to_string(pred),
Expression::ExprPropertyAccess { expr, property } => {
format!("{}.{}", expression_to_string(expr), property)
}
Expression::CountSubquery { .. } => {
"count{...}".to_string()
}
Expression::Reduce {
accumulator,
variable,
list_expr,
..
} => format!(
"reduce({} = ..., {} IN {} | ...)",
accumulator,
variable,
expression_to_string(list_expr)
),
}
}
pub(super) fn predicate_to_string(pred: &Predicate) -> String {
match pred {
Predicate::Comparison {
left,
operator,
right,
} => {
let op_str = match operator {
ComparisonOp::Equals => "=",
ComparisonOp::NotEquals => "<>",
ComparisonOp::LessThan => "<",
ComparisonOp::LessThanEq => "<=",
ComparisonOp::GreaterThan => ">",
ComparisonOp::GreaterThanEq => ">=",
ComparisonOp::RegexMatch => "=~",
};
format!(
"{} {} {}",
expression_to_string(left),
op_str,
expression_to_string(right)
)
}
Predicate::StartsWith { expr, pattern } => {
format!(
"{} STARTS WITH {}",
expression_to_string(expr),
expression_to_string(pattern)
)
}
Predicate::EndsWith { expr, pattern } => {
format!(
"{} ENDS WITH {}",
expression_to_string(expr),
expression_to_string(pattern)
)
}
Predicate::Contains { expr, pattern } => {
format!(
"{} CONTAINS {}",
expression_to_string(expr),
expression_to_string(pattern)
)
}
Predicate::LabelCheck {
variable, label, ..
} => format!("{}:{}", variable, label),
_ => "predicate(...)".to_string(),
}
}
pub(super) fn evaluate_comparison(
left: &Value,
op: &ComparisonOp,
right: &Value,
) -> Result<bool, String> {
match op {
ComparisonOp::Equals => Ok(crate::graph::core::filtering::values_equal(left, right)),
ComparisonOp::NotEquals => Ok(!crate::graph::core::filtering::values_equal(left, right)),
_ if matches!(left, Value::Null) || matches!(right, Value::Null) => Ok(false),
ComparisonOp::LessThan => Ok(crate::graph::core::filtering::compare_values(left, right)
== Some(std::cmp::Ordering::Less)),
ComparisonOp::LessThanEq => Ok(matches!(
crate::graph::core::filtering::compare_values(left, right),
Some(std::cmp::Ordering::Less) | Some(std::cmp::Ordering::Equal)
)),
ComparisonOp::GreaterThan => Ok(crate::graph::core::filtering::compare_values(left, right)
== Some(std::cmp::Ordering::Greater)),
ComparisonOp::GreaterThanEq => Ok(matches!(
crate::graph::core::filtering::compare_values(left, right),
Some(std::cmp::Ordering::Greater) | Some(std::cmp::Ordering::Equal)
)),
ComparisonOp::RegexMatch => match (left, right) {
(Value::String(text), Value::String(pattern)) => {
super::regex_cache::with_compiled_anchored(pattern, |re| re.is_match(text))
.map_err(|e| super::regex_cache::operator_compile_error(pattern, &e))
}
_ => Ok(false),
},
}
}
pub fn resolve_node_property(node: NodeView<'_>, property: &str, graph: &DirGraph) -> Value {
let node_type_str = node.node_type_str(&graph.interner);
let resolved = graph.resolve_alias(node_type_str, property);
resolve_node_property_resolved(node, resolved, InternedKey::from_str(resolved), graph)
}
pub fn resolve_node_property_unaliased(
node: NodeView<'_>,
property: &str,
graph: &DirGraph,
) -> Value {
resolve_node_property_resolved(node, property, InternedKey::from_str(property), graph)
}
pub fn resolve_node_property_keyed(
node: NodeView<'_>,
property: &str,
key: InternedKey,
graph: &DirGraph,
) -> Value {
resolve_node_property_resolved(node, property, key, graph)
}
#[inline]
fn resolve_node_property_resolved(
node: NodeView<'_>,
resolved: &str,
key: InternedKey,
graph: &DirGraph,
) -> Value {
debug_assert_eq!(key, InternedKey::from_str(resolved));
match resolved {
"id" => node.id().into_owned(),
"title" => node.title().into_owned(),
_ => {
if let Some(val) = node.get_value(key) {
return val;
}
let node_type_str = node.node_type_str(&graph.interner);
if let Some(fb) = soft_alias_fallback(resolved) {
return match fb {
SoftAliasFallback::Title => node.title().into_owned(),
SoftAliasFallback::TypeString => Value::String(node_type_str.to_string()),
};
}
if let Some(config) = graph.get_spatial_config(node_type_str) {
if resolved == "location" {
if let Some((lat_f, lon_f)) = &config.location {
let lat = crate::graph::core::value_operations::value_to_f64(
node.get_property(lat_f).as_deref().unwrap_or(&Value::Null),
);
let lon = crate::graph::core::value_operations::value_to_f64(
node.get_property(lon_f).as_deref().unwrap_or(&Value::Null),
);
if let (Some(lat), Some(lon)) = (lat, lon) {
return Value::Point { lat, lon };
}
}
}
if resolved == "geometry" {
if let Some(geom_f) = &config.geometry {
if let Some(val) = node.get_property_value(geom_f) {
return val;
}
}
}
if let Some((lat_f, lon_f)) = config.points.get(resolved) {
let lat = crate::graph::core::value_operations::value_to_f64(
node.get_property(lat_f).as_deref().unwrap_or(&Value::Null),
);
let lon = crate::graph::core::value_operations::value_to_f64(
node.get_property(lon_f).as_deref().unwrap_or(&Value::Null),
);
if let (Some(lat), Some(lon)) = (lat, lon) {
return Value::Point { lat, lon };
}
}
if let Some(shape_f) = config.shapes.get(resolved) {
if let Some(val) = node.get_property_value(shape_f) {
return val;
}
}
}
Value::Null
}
}
}
pub fn resolve_edge_property(graph: &DirGraph, edge: &EdgeBinding, property: &str) -> Value {
let g = &graph.graph;
if let Some(edge_data) = g.edge_weight(edge.edge_index) {
match property {
"type" | "connection_type" => {
Value::String(edge_data.connection_type_str(&graph.interner).to_string())
}
_ => edge_data
.get_property(property)
.cloned()
.unwrap_or(Value::Null),
}
} else {
Value::Null
}
}
pub(super) fn node_to_map_value(node: NodeView<'_>) -> Value {
node.title().into_owned()
}
trait PropertySink {
const NEEDS_VALUES: bool;
fn insert_with(&mut self, key: &str, value: impl FnOnce() -> Value);
fn insert(&mut self, key: &str, value: Value);
fn contains(&self, key: &str) -> bool;
fn absorb_stored(
&mut self,
node: crate::graph::storage::NodeView<'_>,
graph: &DirGraph,
) -> bool;
}
#[inline]
fn is_virtual_or_reserved(key: &str) -> bool {
key == "id"
|| key == "title"
|| key == "type"
|| crate::graph::schema::is_reserved_provenance_key(key)
}
struct PropsSink(Vec<(PropKey, Value)>);
impl Default for PropsSink {
fn default() -> Self {
Self(Vec::with_capacity(16))
}
}
impl PropsSink {
fn finish(self) -> PropMap {
PropMap::from_pairs(self.0)
}
}
impl PropertySink for PropsSink {
const NEEDS_VALUES: bool = true;
#[inline]
fn insert_with(&mut self, key: &str, value: impl FnOnce() -> Value) {
self.0.push((PropKey::from(key), value()));
}
#[inline]
fn insert(&mut self, key: &str, value: Value) {
self.0.push((PropKey::from(key), value));
}
#[inline]
fn contains(&self, key: &str) -> bool {
self.0.iter().any(|(k, _)| &**k == key)
}
fn absorb_stored(
&mut self,
node: crate::graph::storage::NodeView<'_>,
graph: &DirGraph,
) -> bool {
let mut unresolved_key = false;
for (ik, val) in node.property_pairs() {
let Some(key) = graph.interner.try_resolve(ik) else {
unresolved_key = true;
continue;
};
if is_virtual_or_reserved(key) {
continue;
}
self.0.push((PropKey::from(key), val));
}
unresolved_key
}
}
#[derive(Default)]
struct KeySink(std::collections::BTreeSet<String>);
impl PropertySink for KeySink {
const NEEDS_VALUES: bool = false;
#[inline]
fn insert_with(&mut self, key: &str, _value: impl FnOnce() -> Value) {
self.0.insert(key.to_string());
}
#[inline]
fn insert(&mut self, key: &str, _value: Value) {
self.0.insert(key.to_string());
}
#[inline]
fn contains(&self, key: &str) -> bool {
self.0.contains(key)
}
fn absorb_stored(
&mut self,
node: crate::graph::storage::NodeView<'_>,
graph: &DirGraph,
) -> bool {
let mut unresolved_key = false;
for ik in node.property_key_set() {
let Some(key) = graph.interner.try_resolve(ik) else {
unresolved_key = true;
continue;
};
if is_virtual_or_reserved(key) {
continue;
}
self.0.insert(key.to_string());
}
unresolved_key
}
}
#[inline]
fn insert_field_alias<S: PropertySink>(
properties: &mut S,
alias: Option<&String>,
value: impl FnOnce() -> Value,
) {
let Some(alias) = alias else { return };
if alias == "id" || alias == "title" || alias == "type" {
return;
}
if properties.contains(alias) {
return;
}
let v = value();
if !matches!(v, Value::Null) {
properties.insert(alias, v);
}
}
pub(crate) fn materialize_node_value(
idx: petgraph::graph::NodeIndex,
graph: &crate::graph::DirGraph,
) -> Option<crate::datatypes::values::NodeValue> {
if graph.graph.is_disk() {
let data = graph.graph.owned_node_data(idx)?;
let store = data.properties.columnar_row_id().and_then(|row_id| {
graph
.graph
.column_store(data.node_type)
.map(|store| (&**store, row_id))
});
let node = crate::graph::storage::NodeView::new(&data, store);
return Some(node_value_from_view(idx, node, graph));
}
let node = graph.graph.node_view(idx)?;
Some(node_value_from_view(idx, node, graph))
}
pub(crate) fn materialize_node_keys(
idx: petgraph::graph::NodeIndex,
graph: &crate::graph::DirGraph,
) -> Option<Vec<String>> {
if graph.graph.is_disk() {
let data = graph.graph.owned_node_data(idx)?;
let store = data.properties.columnar_row_id().and_then(|row_id| {
graph
.graph
.column_store(data.node_type)
.map(|store| (&**store, row_id))
});
let node = crate::graph::storage::NodeView::new(&data, store);
return Some(node_keys_from_view(node, graph));
}
let node = graph.graph.node_view(idx)?;
Some(node_keys_from_view(node, graph))
}
fn node_keys_from_view(
node: crate::graph::storage::NodeView<'_>,
graph: &crate::graph::DirGraph,
) -> Vec<String> {
let node_type = node.node_type_str(&graph.interner).to_string();
let mut keys = KeySink::default();
collect_node_properties(&mut keys, node, &node_type, graph);
keys.0.into_iter().collect()
}
fn collect_node_properties<S: PropertySink>(
sink: &mut S,
node: crate::graph::storage::NodeView<'_>,
node_type: &str,
graph: &crate::graph::DirGraph,
) {
sink.insert_with("id", || node.id().into_owned());
sink.insert_with("title", || node.title().into_owned());
sink.insert_with("type", || Value::String(node_type.to_string()));
if S::NEEDS_VALUES {
if let Some(stored) = node.get_property_value("type") {
sink.insert("type", stored);
}
}
let unresolved_key = sink.absorb_stored(node, graph);
insert_field_alias(sink, graph.title_field_aliases.get(node_type), || {
node.title().into_owned()
});
insert_field_alias(sink, graph.id_field_aliases.get(node_type), || {
node.id().into_owned()
});
if node.properties_are_columnar() {
if let Some(type_meta) = graph.get_node_type_metadata(node_type) {
complete_from_type_schema(sink, node, node_type, type_meta, graph, unresolved_key);
}
}
}
fn node_value_from_view(
idx: petgraph::graph::NodeIndex,
node: crate::graph::storage::NodeView<'_>,
graph: &crate::graph::DirGraph,
) -> crate::datatypes::values::NodeValue {
use crate::datatypes::values::NodeValue;
let node_type = node.node_type_str(&graph.interner).to_string();
let mut properties = PropsSink::default();
collect_node_properties(&mut properties, node, &node_type, graph);
let properties = properties.finish();
let labels: Vec<String> = graph
.node_labels(idx)
.iter()
.map(|k| graph.interner.resolve(*k).to_string())
.collect();
let labels = if labels.is_empty() {
vec![node_type]
} else {
labels
};
NodeValue {
id: idx.index() as u32,
labels,
properties,
}
}
fn complete_from_type_schema<S: PropertySink>(
properties: &mut S,
node: crate::graph::storage::NodeView<'_>,
node_type: &str,
type_meta: &HashMap<String, String>,
graph: &crate::graph::DirGraph,
unresolved_key: bool,
) {
let complete = |properties: &mut S, prop_name: &String| {
if is_virtual_or_reserved(prop_name) {
return;
}
if properties.contains(prop_name) {
return;
}
let val = resolve_node_property(node, prop_name, graph);
if !matches!(val, Value::Null) {
properties.insert(prop_name, val);
}
};
if unresolved_key {
for prop_name in type_meta.keys() {
complete(properties, prop_name);
}
return;
}
for candidate in crate::graph::schema::SOFT_ALIAS_NAMES {
if let Some((declared, _)) = type_meta.get_key_value(candidate) {
complete(properties, declared);
}
}
if graph.spatial_configs.is_empty() {
return;
}
let Some(config) = graph.get_spatial_config(node_type) else {
return;
};
let named = config
.points
.keys()
.chain(config.shapes.keys())
.map(String::as_str);
for candidate in ["location", "geometry"].into_iter().chain(named) {
if let Some((declared, _)) = type_meta.get_key_value(candidate) {
complete(properties, declared);
}
}
}
pub(crate) fn materialize_rel_value(
edge_idx: petgraph::graph::EdgeIndex,
graph: &crate::graph::DirGraph,
) -> Option<crate::datatypes::values::RelValue> {
use crate::datatypes::values::RelValue;
let edge_data = graph.graph.edge_weight(edge_idx)?;
let (src, dst) = graph.graph.edge_endpoints(edge_idx)?;
let mut properties: Vec<(PropKey, Value)> = Vec::with_capacity(edge_data.properties.len());
for (ik, val) in &edge_data.properties {
let Some(key) = graph.interner.try_resolve(*ik) else {
continue;
};
if crate::graph::schema::is_reserved_provenance_key(key) {
continue;
}
properties.push((PropKey::from(key), val.clone()));
}
let properties = PropMap::from_pairs(properties);
Some(RelValue {
id: edge_idx.index() as u32,
start_id: src.index() as u32,
end_id: dst.index() as u32,
rel_type: edge_data.connection_type_str(&graph.interner).to_string(),
properties,
})
}
pub(crate) fn materialize_path_value(
path: &super::PathBinding,
graph: &crate::graph::DirGraph,
) -> crate::datatypes::values::PathValue {
use crate::datatypes::values::PathValue;
let mut nodes = Vec::with_capacity(path.path.len() + 1);
let mut rels = Vec::with_capacity(path.path.len());
if let Some(src_node) = materialize_node_value(path.source, graph) {
nodes.push(src_node);
}
for hop in &path.path {
if let Some(rel) = materialize_rel_value(hop.edge, graph) {
rels.push(rel);
}
if let Some(node) = materialize_node_value(hop.node, graph) {
nodes.push(node);
}
}
PathValue { nodes, rels }
}
pub(super) fn map_subscript(container: &Value, key: &str) -> Value {
match container {
Value::Map(map) => map.get(key).cloned().unwrap_or(Value::Null),
Value::Node(node) => node.properties.get(key).cloned().unwrap_or(Value::Null),
Value::Relationship(rel) => rel.properties.get(key).cloned().unwrap_or(Value::Null),
_ => Value::Null,
}
}
pub(in crate::graph::languages::cypher) fn index_into_value(
container: &Value,
integer_index: i64,
) -> Value {
match container {
Value::List(items) => index_list_slice(items, integer_index),
Value::String(_) => index_list_slice(&parse_list_value(container), integer_index),
_ => Value::Null,
}
}
#[inline]
fn index_list_slice(items: &[Value], integer_index: i64) -> Value {
let len = items.len() as i64;
let actual_index = if integer_index < 0 {
len + integer_index
} else {
integer_index
};
if actual_index >= 0 && (actual_index as usize) < items.len() {
items[actual_index as usize].clone()
} else {
Value::Null
}
}
pub(in crate::graph::languages::cypher) fn parse_list_value(val: &Value) -> Vec<Value> {
match val {
Value::List(items) => items.clone(),
Value::String(s) => {
let trimmed = s.trim();
if !trimmed.starts_with('[') || !trimmed.ends_with(']') {
return vec![];
}
let inner = &trimmed[1..trimmed.len() - 1];
if inner.is_empty() {
return vec![];
}
let items = split_top_level_commas(inner);
items.into_iter().map(parse_value_token).collect()
}
_ => vec![],
}
}
pub(super) fn parse_value_token(s: &str) -> Value {
let trimmed = s.trim();
if trimmed.is_empty() {
return Value::Null;
}
if let Ok(i) = trimmed.parse::<i64>() {
return Value::Int64(i);
}
if let Ok(f) = trimmed.parse::<f64>() {
return Value::Float64(f);
}
match trimmed {
"true" => return Value::Boolean(true),
"false" => return Value::Boolean(false),
"null" => return Value::Null,
_ => {}
}
let bytes = trimmed.as_bytes();
if bytes.len() >= 2
&& (bytes[0] == b'"' || bytes[0] == b'\'')
&& bytes[bytes.len() - 1] == bytes[0]
{
let inner = &trimmed[1..trimmed.len() - 1];
if let Some(idx_str) = inner.strip_prefix("__nref:") {
if let Ok(idx) = idx_str.parse::<u32>() {
return Value::NodeRef(idx);
}
}
let mut out = String::with_capacity(inner.len());
let mut chars = inner.chars();
while let Some(c) = chars.next() {
if c == '\\' {
if let Some(next) = chars.next() {
out.push(next);
continue;
}
}
out.push(c);
}
return Value::String(out);
}
Value::String(trimmed.to_string())
}
pub(super) fn extract_map_field(s: &str, key: &str) -> Option<Value> {
let trimmed = s.trim();
let bytes = trimmed.as_bytes();
if bytes.len() < 2 || bytes[0] != b'{' || bytes[bytes.len() - 1] != b'}' {
return None;
}
let inner = &trimmed[1..trimmed.len() - 1];
if inner.trim().is_empty() {
return None;
}
for entry in split_top_level_commas(inner) {
let colon_pos = first_top_level_colon(entry)?;
let raw_key = entry[..colon_pos].trim();
let raw_val = entry[colon_pos + 1..].trim();
if let Value::String(parsed_key) = parse_value_token(raw_key) {
if parsed_key == key {
return Some(parse_value_token(raw_val));
}
}
}
None
}
pub(super) fn point_field(val: &Value, property: &str) -> Value {
if let Value::Point { lat, lon } = val {
return match property {
"latitude" | "lat" | "y" => Value::Float64(*lat),
"longitude" | "lon" | "lng" | "long" | "x" => Value::Float64(*lon),
_ => Value::Null,
};
}
Value::Null
}
fn first_top_level_colon(s: &str) -> Option<usize> {
let mut depth = 0i32;
let mut in_quotes = false;
let mut quote_char = '"';
for (i, ch) in s.char_indices() {
match ch {
'"' | '\'' if !in_quotes => {
in_quotes = true;
quote_char = ch;
}
c if in_quotes && c == quote_char => {
let bytes = s.as_bytes();
if i == 0 || bytes[i - 1] != b'\\' {
in_quotes = false;
}
}
'{' | '[' | '(' if !in_quotes => depth += 1,
'}' | ']' | ')' if !in_quotes => depth -= 1,
':' if !in_quotes && depth == 0 => return Some(i),
_ => {}
}
}
None
}
pub(super) fn split_top_level_commas(s: &str) -> Vec<&str> {
let mut items = Vec::new();
let mut depth = 0i32; let mut in_quotes = false;
let mut quote_char = '"';
let mut start = 0;
for (i, ch) in s.char_indices() {
match ch {
'"' | '\'' if !in_quotes => {
in_quotes = true;
quote_char = ch;
}
c if in_quotes && c == quote_char => {
let bytes = s.as_bytes();
if i == 0 || bytes[i - 1] != b'\\' {
in_quotes = false;
}
}
'{' | '[' | '(' if !in_quotes => depth += 1,
'}' | ']' | ')' if !in_quotes => depth -= 1,
',' if !in_quotes && depth == 0 => {
items.push(&s[start..i]);
start = i + 1;
}
_ => {}
}
}
items.push(&s[start..]);
items
}
pub(super) fn format_value_compact(val: &Value) -> String {
crate::graph::core::value_operations::format_value_compact(val)
}
pub(super) fn value_to_f64(val: &Value) -> Option<f64> {
crate::graph::core::value_operations::value_to_f64(val)
}
pub(super) fn coerce_to_string(val: Value) -> Value {
match &val {
Value::String(_) | Value::Null => val,
_ => Value::String(format_value_compact(&val)),
}
}
pub(super) fn levenshtein(a: &str, b: &str) -> usize {
if a == b {
return 0;
}
let a_chars: Vec<char> = a.chars().collect();
let b_chars: Vec<char> = b.chars().collect();
if a_chars.is_empty() {
return b_chars.len();
}
if b_chars.is_empty() {
return a_chars.len();
}
let (short, long) = if a_chars.len() <= b_chars.len() {
(&a_chars, &b_chars)
} else {
(&b_chars, &a_chars)
};
let mut prev: Vec<usize> = (0..=short.len()).collect();
let mut curr: Vec<usize> = vec![0; short.len() + 1];
for (i, lc) in long.iter().enumerate() {
curr[0] = i + 1;
for (j, sc) in short.iter().enumerate() {
let cost = if lc == sc { 0 } else { 1 };
curr[j + 1] = (curr[j] + 1).min(prev[j + 1] + 1).min(prev[j] + cost);
}
std::mem::swap(&mut prev, &mut curr);
}
prev[short.len()]
}
pub(super) fn parse_json_float_list(s: &str) -> Result<Vec<f32>, String> {
let trimmed = s.trim();
if !trimmed.starts_with('[') || !trimmed.ends_with(']') {
return Err("vector_score(): query vector must be a list like [1.0, 2.0, ...]".into());
}
let inner = &trimmed[1..trimmed.len() - 1];
if inner.is_empty() {
return Ok(Vec::new());
}
inner
.split(',')
.map(|item| {
item.trim()
.parse::<f32>()
.map_err(|_| format!("vector_score(): cannot parse '{}' as a number", item.trim()))
})
.collect()
}
#[cfg(test)]
pub(super) fn arithmetic_add(a: &Value, b: &Value) -> Value {
crate::graph::core::value_operations::arithmetic_add(a, b)
}
#[cfg(test)]
pub(super) fn arithmetic_sub(a: &Value, b: &Value) -> Value {
crate::graph::core::value_operations::arithmetic_sub(a, b)
}
#[cfg(test)]
pub(super) fn arithmetic_mul(a: &Value, b: &Value) -> Value {
crate::graph::core::value_operations::arithmetic_mul(a, b)
}
pub(super) fn arithmetic_div(a: &Value, b: &Value) -> Result<Value, String> {
crate::graph::core::value_operations::arithmetic_div_checked(a, b)
}
pub(super) fn arithmetic_mod(a: &Value, b: &Value) -> Result<Value, String> {
crate::graph::core::value_operations::arithmetic_mod_checked(a, b)
}
pub(super) fn arithmetic_negate(a: &Value) -> Result<Value, String> {
crate::graph::core::value_operations::arithmetic_negate_checked(a)
}
pub(super) fn to_integer(val: &Value) -> Value {
crate::graph::core::value_operations::to_integer(val)
}
pub(super) fn as_i64(val: &Value) -> Result<i64, String> {
match val {
Value::Int64(n) => Ok(*n),
Value::Float64(f) => Ok(*f as i64),
Value::String(s) => s
.parse::<i64>()
.map_err(|_| format!("Cannot convert '{}' to integer", s)),
_ => Err(format!("Expected integer, got {:?}", val)),
}
}
pub(super) fn to_float(val: &Value) -> Value {
crate::graph::core::value_operations::to_float(val)
}
pub(super) fn parse_value_string(s: &str) -> Value {
crate::graph::core::value_operations::parse_value_string(s)
}
pub(super) fn split_list_top_level(s: &str) -> Vec<&str> {
let trimmed = s.trim();
if !trimmed.starts_with('[') || !trimmed.ends_with(']') || trimmed.len() < 2 {
return Vec::new();
}
let inner = &trimmed[1..trimmed.len() - 1];
if inner.trim().is_empty() {
return Vec::new();
}
let mut items = Vec::new();
let mut depth = 0i32;
let mut in_string = false;
let mut quote_char = '"';
let mut escape = false;
let mut start = 0;
for (i, ch) in inner.char_indices() {
if escape {
escape = false;
continue;
}
match ch {
'\\' if in_string => {
escape = true;
}
'"' | '\'' if !in_string => {
in_string = true;
quote_char = ch;
}
c if in_string && c == quote_char => {
in_string = false;
}
'[' | '{' if !in_string => {
depth += 1;
}
']' | '}' if !in_string => {
depth -= 1;
}
',' if !in_string && depth == 0 => {
items.push(inner[start..i].trim());
start = i + 1;
}
_ => {}
}
}
let last = inner[start..].trim();
if !last.is_empty() {
items.push(last);
}
items
}
pub(super) fn call_param_f64(params: &HashMap<String, Value>, key: &str, default: f64) -> f64 {
params
.get(key)
.map(|v| match v {
Value::Float64(f) => *f,
Value::Int64(i) => *i as f64,
_ => default,
})
.unwrap_or(default)
}
pub(super) fn call_param_usize(
params: &HashMap<String, Value>,
key: &str,
default: usize,
) -> usize {
params
.get(key)
.map(|v| match v {
Value::Int64(i) => *i as usize,
Value::Float64(f) => *f as usize,
_ => default,
})
.unwrap_or(default)
}
pub(super) fn call_param_bool(params: &HashMap<String, Value>, key: &str, default: bool) -> bool {
params
.get(key)
.map(|v| match v {
Value::Boolean(b) => *b,
_ => default,
})
.unwrap_or(default)
}
pub(super) fn call_param_opt_usize(params: &HashMap<String, Value>, key: &str) -> Option<usize> {
params.get(key).and_then(|v| match v {
Value::Int64(i) => Some(*i as usize),
_ => None,
})
}
pub(super) fn call_param_opt_string(params: &HashMap<String, Value>, key: &str) -> Option<String> {
params.get(key).and_then(|v| match v {
Value::String(s) => Some(s.clone()),
_ => None,
})
}
pub(super) fn call_param_string_list(
params: &HashMap<String, Value>,
key: &str,
) -> Option<Vec<String>> {
params.get(key).and_then(|v| match v {
Value::List(items) => {
let strs: Vec<String> = items
.iter()
.filter_map(|item| match item {
Value::String(s) => Some(s.clone()),
_ => None,
})
.collect();
if strs.is_empty() {
None
} else {
Some(strs)
}
}
Value::String(s) => {
if s.starts_with('[') {
let items = parse_list_value(v);
if items.is_empty() {
return None;
}
Some(
items
.into_iter()
.filter_map(|item| match item {
Value::String(s) => Some(s),
_ => None,
})
.collect(),
)
} else {
Some(vec![s.clone()])
}
}
_ => None,
})
}
pub(super) fn call_param_string(params: &HashMap<String, Value>, key: &str) -> Option<String> {
params.get(key).and_then(|v| match v {
Value::String(s) => Some(s.clone()),
_ => None,
})
}
pub(super) fn yield_alias(yield_items: &[YieldItem], expected: &str) -> Option<String> {
yield_items
.iter()
.find(|y| y.name == expected)
.map(|item| item.alias.clone().unwrap_or_else(|| expected.to_string()))
}
#[cfg(test)]
#[path = "node_record_golden_tests.rs"]
mod node_record_golden_tests;
#[cfg(test)]
mod user_input_error_tests {
use super::is_user_input_error;
use crate::graph::languages::cypher::executor::expression::missing_parameter_error;
use crate::graph::languages::cypher::executor::regex_cache::{
function_compile_error, operator_compile_error,
};
#[test]
fn both_propagating_classes_are_recognised() {
let bad = String::from("[");
let err = regex::Regex::new(&bad).expect_err("'[' must not compile");
assert!(is_user_input_error(&operator_compile_error("[", &err)));
assert!(is_user_input_error(&function_compile_error(
"text_match_regex",
&err
)));
assert!(is_user_input_error(&missing_parameter_error("flag")));
assert!(is_user_input_error(&format!(
"{} (used as a label or relationship type)",
missing_parameter_error("label")
)));
}
#[test]
fn the_swallowed_classes_are_not_recognised() {
for message in [
"Cannot evaluate aggregate function in this context",
"Variable 'x' not bound",
"Unknown function: nope",
"",
] {
assert!(!is_user_input_error(message), "{message}");
}
}
}
#[cfg(test)]
mod split_list_top_level_tests {
use super::split_list_top_level;
#[test]
fn a_quote_of_the_other_kind_does_not_close_the_string() {
assert_eq!(split_list_top_level("[\"a,b'\", 2]"), vec!["\"a,b'\"", "2"]);
assert_eq!(split_list_top_level("['a\"b', 2]"), vec!["'a\"b'", "2"]);
}
#[test]
fn input_that_is_not_bracket_wrapped_yields_nothing() {
assert_eq!(split_list_top_level(""), Vec::<&str>::new());
assert_eq!(split_list_top_level("["), Vec::<&str>::new());
assert_eq!(split_list_top_level("1, 2"), Vec::<&str>::new());
}
#[test]
fn the_wrapped_cases_still_split_as_before() {
assert_eq!(split_list_top_level("[]"), Vec::<&str>::new());
assert_eq!(
split_list_top_level("[1, 2, [3, 4], 5]"),
vec!["1", "2", "[3, 4]", "5"]
);
assert_eq!(
split_list_top_level("[\"a\\\",b\", 2]"),
vec!["\"a\\\",b\"", "2"]
);
}
}