#[allow(unused_imports)]
use crate::value::LoraPath;
use crate::value::{LoraValue, Row};
use lora_analyzer::{LiteralValue, ResolvedExpr, ResolvedMapSelector};
use lora_ast::{BinaryOp, ListPredicateKind, UnaryOp};
use lora_store::{
cosine_similarity_bounded, cosine_similarity_raw, dot_product, euclidean_distance,
euclidean_distance_squared, euclidean_norm, euclidean_similarity, hamming_distance,
manhattan_distance, manhattan_norm, parse_string_values, point_distance, resolve_srid,
srid_is_3d, GraphStorage, LoraDate, LoraDateTime, LoraDuration, LoraLocalDateTime,
LoraLocalTime, LoraPoint, LoraTime, LoraVector, PointKeyFamily, RawCoordinate,
VectorCoordinateType,
};
use std::collections::BTreeMap;
pub struct EvalContext<'a, S: GraphStorage> {
pub storage: &'a S,
pub params: &'a BTreeMap<String, LoraValue>,
}
pub fn eval_expr<S: GraphStorage>(
expr: &ResolvedExpr,
row: &Row,
ctx: &EvalContext<'_, S>,
) -> LoraValue {
match expr {
ResolvedExpr::Variable(var_id) => row.get(*var_id).cloned().unwrap_or(LoraValue::Null),
ResolvedExpr::Literal(lit) => eval_literal(lit),
ResolvedExpr::List(items) => {
LoraValue::List(items.iter().map(|e| eval_expr(e, row, ctx)).collect())
}
ResolvedExpr::Map(items) => {
let mut map = std::collections::BTreeMap::new();
for (k, v) in items {
map.insert(k.clone(), eval_expr(v, row, ctx));
}
LoraValue::Map(map)
}
ResolvedExpr::Property { expr, property } => {
let base = eval_expr(expr, row, ctx);
eval_property(&base, property, ctx)
}
ResolvedExpr::Binary { lhs, op, rhs } => {
let l = eval_expr(lhs, row, ctx);
let r = eval_expr(rhs, row, ctx);
eval_binary(op, l, r)
}
ResolvedExpr::Unary { op, expr } => {
let v = eval_expr(expr, row, ctx);
eval_unary(*op, v)
}
ResolvedExpr::Function {
name,
distinct: _,
args,
} => {
let args: Vec<LoraValue> = args.iter().map(|a| eval_expr(a, row, ctx)).collect();
eval_function(name, &args, ctx)
}
ResolvedExpr::Parameter(name) => ctx.params.get(name).cloned().unwrap_or(LoraValue::Null),
ResolvedExpr::ListPredicate {
kind,
variable,
list,
predicate,
} => {
let list_val = eval_expr(list, row, ctx);
match list_val {
LoraValue::List(items) => {
let total = items.len();
let mut inner_row = row.clone();
let mut count = 0usize;
for item in items {
inner_row.insert(*variable, item);
if eval_expr(predicate, &inner_row, ctx).is_truthy() {
count += 1;
}
}
match kind {
ListPredicateKind::Any => LoraValue::Bool(count > 0),
ListPredicateKind::All => LoraValue::Bool(count == total),
ListPredicateKind::None => LoraValue::Bool(count == 0),
ListPredicateKind::Single => LoraValue::Bool(count == 1),
}
}
LoraValue::Null => LoraValue::Null,
_ => LoraValue::Bool(false),
}
}
ResolvedExpr::ListComprehension {
variable,
list,
filter,
map_expr,
} => {
let list_val = eval_expr(list, row, ctx);
match list_val {
LoraValue::List(items) => {
let mut result = Vec::with_capacity(items.len());
let mut inner_row = row.clone();
for item in items {
inner_row.insert(*variable, item);
if let Some(f) = filter {
if !eval_expr(f, &inner_row, ctx).is_truthy() {
continue;
}
}
let val = if let Some(m) = map_expr {
eval_expr(m, &inner_row, ctx)
} else {
inner_row.get(*variable).cloned().unwrap_or(LoraValue::Null)
};
result.push(val);
}
LoraValue::List(result)
}
LoraValue::Null => LoraValue::Null,
_ => LoraValue::Null,
}
}
ResolvedExpr::Reduce {
accumulator,
init,
variable,
list,
expr,
} => {
let init_val = eval_expr(init, row, ctx);
let list_val = eval_expr(list, row, ctx);
match list_val {
LoraValue::List(items) => {
let mut inner_row = row.clone();
let mut acc = init_val;
for item in items {
inner_row.insert(*accumulator, acc);
inner_row.insert(*variable, item);
acc = eval_expr(expr, &inner_row, ctx);
}
acc
}
LoraValue::Null => LoraValue::Null,
_ => LoraValue::Null,
}
}
ResolvedExpr::Index { expr, index } => {
let base = eval_expr(expr, row, ctx);
let idx = eval_expr(index, row, ctx);
match (base, idx) {
(LoraValue::List(items), LoraValue::Int(i)) => {
let i = if i < 0 {
(items.len() as i64 + i) as usize
} else {
i as usize
};
items.get(i).cloned().unwrap_or(LoraValue::Null)
}
(LoraValue::Map(m), LoraValue::String(key)) => {
m.get(&key).cloned().unwrap_or(LoraValue::Null)
}
_ => LoraValue::Null,
}
}
ResolvedExpr::Slice { expr, from, to } => {
let base = eval_expr(expr, row, ctx);
match base {
LoraValue::List(items) => {
let len = items.len() as i64;
let start = from
.as_ref()
.map(|e| eval_expr(e, row, ctx).as_i64().unwrap_or(0))
.unwrap_or(0)
.max(0)
.min(len) as usize;
let end = to
.as_ref()
.map(|e| eval_expr(e, row, ctx).as_i64().unwrap_or(len))
.unwrap_or(len)
.max(0)
.min(len) as usize;
if start >= end {
LoraValue::List(Vec::new())
} else {
LoraValue::List(items[start..end].to_vec())
}
}
_ => LoraValue::Null,
}
}
ResolvedExpr::MapProjection { base, selectors } => {
let base_val = eval_expr(base, row, ctx);
let mut result = std::collections::BTreeMap::new();
for sel in selectors {
match sel {
ResolvedMapSelector::Property(key) => {
let val = eval_property(&base_val, key, ctx);
result.insert(key.clone(), val);
}
ResolvedMapSelector::AllProperties => {
match &base_val {
LoraValue::Node(id) => {
ctx.storage.with_node(*id, |node| {
for (k, v) in &node.properties {
result.insert(k.clone(), LoraValue::from(v));
}
});
}
LoraValue::Relationship(id) => {
ctx.storage.with_relationship(*id, |rel| {
for (k, v) in &rel.properties {
result.insert(k.clone(), LoraValue::from(v));
}
});
}
LoraValue::Map(m) => {
for (k, v) in m {
result.insert(k.clone(), v.clone());
}
}
_ => {}
}
}
ResolvedMapSelector::Literal(key, expr) => {
let val = eval_expr(expr, row, ctx);
result.insert(key.clone(), val);
}
}
}
LoraValue::Map(result)
}
ResolvedExpr::Case {
input,
alternatives,
else_expr,
} => {
if let Some(input) = input {
let input_val = eval_expr(input, row, ctx);
for (when, then) in alternatives {
let when_val = eval_expr(when, row, ctx);
if value_eq(&input_val, &when_val) {
return eval_expr(then, row, ctx);
}
}
else_expr
.as_ref()
.map(|e| eval_expr(e, row, ctx))
.unwrap_or(LoraValue::Null)
} else {
for (when, then) in alternatives {
let when_val = eval_expr(when, row, ctx);
if when_val.is_truthy() {
return eval_expr(then, row, ctx);
}
}
else_expr
.as_ref()
.map(|e| eval_expr(e, row, ctx))
.unwrap_or(LoraValue::Null)
}
}
ResolvedExpr::ExistsSubquery { pattern, where_ } => {
eval_exists_subquery(pattern, where_.as_deref(), row, ctx)
}
ResolvedExpr::PatternComprehension {
pattern,
where_,
map_expr,
} => eval_pattern_comprehension(pattern, where_.as_deref(), map_expr, row, ctx),
}
}
fn eval_exists_subquery<S: GraphStorage>(
pattern: &lora_analyzer::ResolvedPattern,
where_: Option<&ResolvedExpr>,
row: &Row,
ctx: &EvalContext<'_, S>,
) -> LoraValue {
use lora_analyzer::ResolvedPatternElement;
let mut candidate_rows = vec![row.clone()];
for part in &pattern.parts {
let mut next_rows = Vec::new();
for current_row in &candidate_rows {
match &part.element {
ResolvedPatternElement::Node {
var,
labels,
properties,
} => {
let tmp_node = lora_analyzer::ResolvedNode {
var: *var,
labels: labels.clone(),
properties: properties.clone(),
};
next_rows.extend(match_node_pattern(&tmp_node, current_row, ctx));
}
ResolvedPatternElement::ShortestPath { head, chain, .. }
| ResolvedPatternElement::NodeChain { head, chain } => {
let head_rows = match_node_pattern(head, current_row, ctx);
for hr in head_rows {
let mut frontier = vec![hr];
for step in chain {
let mut step_rows = Vec::new();
for fr in &frontier {
let src_node_id = find_last_node_in_row(fr, head.var, chain, step);
if let Some(sid) = src_node_id {
for (rel_id, dst_id) in ctx.storage.expand_ids(
sid,
step.rel.direction,
&step.rel.types,
) {
let matched = ctx
.storage
.with_node(dst_id, |dst| {
node_matches_labels(&dst.labels, &step.node.labels)
&& node_matches_properties(
&dst.properties,
&step.node.properties,
fr,
ctx,
)
})
.unwrap_or(false);
if !matched {
continue;
}
let mut r = fr.clone();
if let Some(rv) = step.rel.var {
r.insert(rv, LoraValue::Relationship(rel_id));
}
if let Some(nv) = step.node.var {
r.insert(nv, LoraValue::Node(dst_id));
}
step_rows.push(r);
}
}
}
frontier = step_rows;
}
next_rows.extend(frontier);
}
}
}
}
candidate_rows = next_rows;
}
if let Some(where_expr) = where_ {
candidate_rows.retain(|r| eval_expr(where_expr, r, ctx).is_truthy());
}
LoraValue::Bool(!candidate_rows.is_empty())
}
fn eval_pattern_comprehension<S: GraphStorage>(
pattern: &lora_analyzer::ResolvedPattern,
where_: Option<&ResolvedExpr>,
map_expr: &ResolvedExpr,
row: &Row,
ctx: &EvalContext<'_, S>,
) -> LoraValue {
let mut candidate_rows = vec![row.clone()];
for part in &pattern.parts {
let mut next_rows = Vec::new();
for current_row in &candidate_rows {
match &part.element {
lora_analyzer::ResolvedPatternElement::Node {
var,
labels,
properties,
} => {
let tmp_node = lora_analyzer::ResolvedNode {
var: *var,
labels: labels.clone(),
properties: properties.clone(),
};
next_rows.extend(match_node_pattern(&tmp_node, current_row, ctx));
}
lora_analyzer::ResolvedPatternElement::ShortestPath { head, chain, .. }
| lora_analyzer::ResolvedPatternElement::NodeChain { head, chain } => {
let head_rows = match_node_pattern(head, current_row, ctx);
for hr in head_rows {
let mut frontier = vec![hr];
for step in chain {
let mut step_rows = Vec::new();
for fr in &frontier {
let src_node_id = find_last_node_in_row(fr, head.var, chain, step);
if let Some(sid) = src_node_id {
for (rel_id, dst_id) in ctx.storage.expand_ids(
sid,
step.rel.direction,
&step.rel.types,
) {
let matched = ctx
.storage
.with_node(dst_id, |dst| {
node_matches_labels(&dst.labels, &step.node.labels)
&& node_matches_properties(
&dst.properties,
&step.node.properties,
fr,
ctx,
)
})
.unwrap_or(false);
if !matched {
continue;
}
let mut r = fr.clone();
if let Some(rv) = step.rel.var {
r.insert(rv, LoraValue::Relationship(rel_id));
}
if let Some(nv) = step.node.var {
r.insert(nv, LoraValue::Node(dst_id));
}
step_rows.push(r);
}
}
}
frontier = step_rows;
}
next_rows.extend(frontier);
}
}
}
}
candidate_rows = next_rows;
}
if let Some(where_expr) = where_ {
candidate_rows.retain(|r| eval_expr(where_expr, r, ctx).is_truthy());
}
LoraValue::List(
candidate_rows
.iter()
.map(|r| eval_expr(map_expr, r, ctx))
.collect(),
)
}
fn match_node_pattern<S: GraphStorage>(
node: &lora_analyzer::ResolvedNode,
row: &Row,
ctx: &EvalContext<'_, S>,
) -> Vec<Row> {
if let Some(var) = node.var {
if let Some(LoraValue::Node(id)) = row.get(var) {
let matched = ctx
.storage
.with_node(*id, |n| {
node_matches_labels(&n.labels, &node.labels)
&& node_matches_properties(&n.properties, &node.properties, row, ctx)
})
.unwrap_or(false);
if matched {
return vec![row.clone()];
}
return Vec::new();
}
}
let first_label = node.labels.iter().flat_map(|g| g.iter()).next();
let candidate_ids: Vec<lora_store::NodeId> = match first_label {
Some(label) => ctx.storage.node_ids_by_label(label),
None => ctx.storage.all_node_ids(),
};
let mut out = Vec::new();
for id in candidate_ids {
let matched = ctx
.storage
.with_node(id, |n| {
node_matches_labels(&n.labels, &node.labels)
&& node_matches_properties(&n.properties, &node.properties, row, ctx)
})
.unwrap_or(false);
if !matched {
continue;
}
let mut r = row.clone();
if let Some(v) = node.var {
r.insert(v, LoraValue::Node(id));
}
out.push(r);
}
out
}
fn find_last_node_in_row(
row: &Row,
head_var: Option<lora_analyzer::symbols::VarId>,
chain: &[lora_analyzer::ResolvedChain],
current_step: &lora_analyzer::ResolvedChain,
) -> Option<u64> {
let mut prev_var = head_var;
for step in chain {
if std::ptr::eq(step, current_step) {
break;
}
prev_var = step.node.var;
}
prev_var.and_then(|v| match row.get(v) {
Some(LoraValue::Node(id)) => Some(*id),
_ => None,
})
}
fn node_matches_labels(node_labels: &[String], groups: &[Vec<String>]) -> bool {
groups
.iter()
.all(|group| group.iter().any(|l| node_labels.iter().any(|nl| nl == l)))
}
fn node_matches_properties<S: GraphStorage>(
props: &std::collections::BTreeMap<String, lora_store::PropertyValue>,
expected: &Option<ResolvedExpr>,
row: &Row,
ctx: &EvalContext<'_, S>,
) -> bool {
let Some(props_expr) = expected else {
return true;
};
let expected = eval_expr(props_expr, row, ctx);
if let LoraValue::Map(exp) = expected {
exp.iter().all(|(k, v)| {
props
.get(k)
.map(|pv| crate::executor::value_matches_property_value(v, pv))
.unwrap_or(false)
})
} else {
true
}
}
fn eval_literal(lit: &LiteralValue) -> LoraValue {
match lit {
LiteralValue::Integer(v) => LoraValue::Int(*v),
LiteralValue::Float(v) => LoraValue::Float(*v),
LiteralValue::String(v) => LoraValue::String(v.clone()),
LiteralValue::Bool(v) => LoraValue::Bool(*v),
LiteralValue::Null => LoraValue::Null,
}
}
fn eval_property<S: GraphStorage>(
base: &LoraValue,
key: &str,
ctx: &EvalContext<'_, S>,
) -> LoraValue {
match base {
LoraValue::Map(map) => {
if let Some(v) = map.get(key) {
return v.clone();
}
if let Some(LoraValue::Map(props)) = map.get("properties") {
if let Some(v) = props.get(key) {
return v.clone();
}
}
LoraValue::Null
}
LoraValue::Node(id) => ctx
.storage
.with_node(*id, |node| {
node.properties
.get(key)
.map(LoraValue::from)
.unwrap_or(LoraValue::Null)
})
.unwrap_or(LoraValue::Null),
LoraValue::Relationship(id) => ctx
.storage
.with_relationship(*id, |rel| {
rel.properties
.get(key)
.map(LoraValue::from)
.unwrap_or(LoraValue::Null)
})
.unwrap_or(LoraValue::Null),
LoraValue::Date(d) => match key {
"year" => LoraValue::Int(d.year as i64),
"month" => LoraValue::Int(d.month as i64),
"day" => LoraValue::Int(d.day as i64),
"dayOfWeek" => LoraValue::Int(d.day_of_week() as i64),
"dayOfYear" => LoraValue::Int(d.day_of_year() as i64),
_ => LoraValue::Null,
},
LoraValue::DateTime(dt) => match key {
"year" => LoraValue::Int(dt.year as i64),
"month" => LoraValue::Int(dt.month as i64),
"day" => LoraValue::Int(dt.day as i64),
"hour" => LoraValue::Int(dt.hour as i64),
"minute" => LoraValue::Int(dt.minute as i64),
"second" => LoraValue::Int(dt.second as i64),
"millisecond" => LoraValue::Int((dt.nanosecond / 1_000_000) as i64),
"dayOfWeek" => LoraValue::Int(dt.date().day_of_week() as i64),
"dayOfYear" => LoraValue::Int(dt.date().day_of_year() as i64),
_ => LoraValue::Null,
},
LoraValue::LocalDateTime(dt) => match key {
"year" => LoraValue::Int(dt.year as i64),
"month" => LoraValue::Int(dt.month as i64),
"day" => LoraValue::Int(dt.day as i64),
"hour" => LoraValue::Int(dt.hour as i64),
"minute" => LoraValue::Int(dt.minute as i64),
"second" => LoraValue::Int(dt.second as i64),
"millisecond" => LoraValue::Int((dt.nanosecond / 1_000_000) as i64),
_ => LoraValue::Null,
},
LoraValue::Time(t) => match key {
"hour" => LoraValue::Int(t.hour as i64),
"minute" => LoraValue::Int(t.minute as i64),
"second" => LoraValue::Int(t.second as i64),
"millisecond" => LoraValue::Int((t.nanosecond / 1_000_000) as i64),
_ => LoraValue::Null,
},
LoraValue::LocalTime(t) => match key {
"hour" => LoraValue::Int(t.hour as i64),
"minute" => LoraValue::Int(t.minute as i64),
"second" => LoraValue::Int(t.second as i64),
"millisecond" => LoraValue::Int((t.nanosecond / 1_000_000) as i64),
_ => LoraValue::Null,
},
LoraValue::Duration(dur) => match key {
"years" => LoraValue::Int(dur.years_component()),
"months" => LoraValue::Int(dur.months_component()),
"days" => LoraValue::Int(dur.days_component()),
"hours" => LoraValue::Int(dur.hours_component()),
"minutes" => LoraValue::Int(dur.minutes_component()),
"seconds" => LoraValue::Int(dur.seconds_component()),
_ => LoraValue::Null,
},
LoraValue::Point(p) => match key {
"x" => LoraValue::Float(p.x),
"y" => LoraValue::Float(p.y),
"z" => p.z.map(LoraValue::Float).unwrap_or(LoraValue::Null),
"latitude" => {
if p.is_geographic() {
LoraValue::Float(p.latitude())
} else {
LoraValue::Null
}
}
"longitude" => {
if p.is_geographic() {
LoraValue::Float(p.longitude())
} else {
LoraValue::Null
}
}
"height" => p.height().map(LoraValue::Float).unwrap_or(LoraValue::Null),
"srid" => LoraValue::Int(p.srid as i64),
"crs" => LoraValue::String(p.crs_name().to_string()),
_ => LoraValue::Null,
},
_ => LoraValue::Null,
}
}
fn eval_unary(op: UnaryOp, value: LoraValue) -> LoraValue {
match op {
UnaryOp::Not => {
if matches!(value, LoraValue::Null) {
LoraValue::Null
} else {
LoraValue::Bool(!value.is_truthy())
}
}
UnaryOp::Pos => value,
UnaryOp::Neg => match value {
LoraValue::Int(v) => LoraValue::Int(-v),
LoraValue::Float(v) => LoraValue::Float(-v),
_ => LoraValue::Null,
},
}
}
fn eval_binary(op: &BinaryOp, lhs: LoraValue, rhs: LoraValue) -> LoraValue {
match op {
BinaryOp::And => {
let l_null = matches!(lhs, LoraValue::Null);
let r_null = matches!(rhs, LoraValue::Null);
if l_null || r_null {
if (!l_null && !lhs.is_truthy()) || (!r_null && !rhs.is_truthy()) {
LoraValue::Bool(false)
} else {
LoraValue::Null
}
} else {
LoraValue::Bool(lhs.is_truthy() && rhs.is_truthy())
}
}
BinaryOp::Or => {
let l_null = matches!(lhs, LoraValue::Null);
let r_null = matches!(rhs, LoraValue::Null);
if l_null || r_null {
if (!l_null && lhs.is_truthy()) || (!r_null && rhs.is_truthy()) {
LoraValue::Bool(true)
} else {
LoraValue::Null
}
} else {
LoraValue::Bool(lhs.is_truthy() || rhs.is_truthy())
}
}
BinaryOp::Xor => {
if matches!(lhs, LoraValue::Null) || matches!(rhs, LoraValue::Null) {
LoraValue::Null
} else {
LoraValue::Bool(lhs.is_truthy() ^ rhs.is_truthy())
}
}
BinaryOp::Eq => {
if matches!(lhs, LoraValue::Null) || matches!(rhs, LoraValue::Null) {
LoraValue::Null
} else {
LoraValue::Bool(value_eq(&lhs, &rhs))
}
}
BinaryOp::Ne => {
if matches!(lhs, LoraValue::Null) || matches!(rhs, LoraValue::Null) {
LoraValue::Null
} else {
LoraValue::Bool(!value_eq(&lhs, &rhs))
}
}
BinaryOp::Lt | BinaryOp::Gt | BinaryOp::Le | BinaryOp::Ge => {
if matches!(lhs, LoraValue::Null) || matches!(rhs, LoraValue::Null) {
return LoraValue::Null;
}
match op {
BinaryOp::Lt => cmp_numeric_or_string(lhs, rhs, |a, b| a < b, |a, b| a < b),
BinaryOp::Gt => cmp_numeric_or_string(lhs, rhs, |a, b| a > b, |a, b| a > b),
BinaryOp::Le => cmp_numeric_or_string(lhs, rhs, |a, b| a <= b, |a, b| a <= b),
BinaryOp::Ge => cmp_numeric_or_string(lhs, rhs, |a, b| a >= b, |a, b| a >= b),
_ => unreachable!(),
}
}
BinaryOp::Add => add_values(lhs, rhs),
BinaryOp::Sub => sub_values(lhs, rhs),
BinaryOp::Mul => mul_values(lhs, rhs),
BinaryOp::Div => div_values(lhs, rhs),
BinaryOp::Mod => mod_values(lhs, rhs),
BinaryOp::Pow => pow_values(lhs, rhs),
BinaryOp::In => {
if matches!(lhs, LoraValue::Null) {
return LoraValue::Null;
}
match rhs {
LoraValue::List(values) => {
LoraValue::Bool(values.iter().any(|v| value_eq(&lhs, v)))
}
LoraValue::Null => LoraValue::Null,
_ => LoraValue::Bool(false),
}
}
BinaryOp::StartsWith => match (lhs, rhs) {
(LoraValue::Null, _) | (_, LoraValue::Null) => LoraValue::Null,
(LoraValue::String(a), LoraValue::String(b)) => LoraValue::Bool(a.starts_with(&b)),
_ => LoraValue::Bool(false),
},
BinaryOp::EndsWith => match (lhs, rhs) {
(LoraValue::Null, _) | (_, LoraValue::Null) => LoraValue::Null,
(LoraValue::String(a), LoraValue::String(b)) => LoraValue::Bool(a.ends_with(&b)),
_ => LoraValue::Bool(false),
},
BinaryOp::Contains => match (lhs, rhs) {
(LoraValue::Null, _) | (_, LoraValue::Null) => LoraValue::Null,
(LoraValue::String(a), LoraValue::String(b)) => LoraValue::Bool(a.contains(&b)),
(LoraValue::List(a), b) => LoraValue::Bool(a.iter().any(|v| value_eq(v, &b))),
_ => LoraValue::Bool(false),
},
BinaryOp::IsNull => LoraValue::Bool(matches!(lhs, LoraValue::Null)),
BinaryOp::IsNotNull => LoraValue::Bool(!matches!(lhs, LoraValue::Null)),
BinaryOp::RegexMatch => match (lhs, rhs) {
(LoraValue::Null, _) | (_, LoraValue::Null) => LoraValue::Null,
(LoraValue::String(s), LoraValue::String(pattern)) => {
match regex::Regex::new(&format!("^(?:{pattern})$")) {
Ok(re) => LoraValue::Bool(re.is_match(&s)),
Err(_) => LoraValue::Null,
}
}
_ => LoraValue::Bool(false),
},
}
}
fn eval_function<S: GraphStorage>(
name: &str,
args: &[LoraValue],
ctx: &EvalContext<'_, S>,
) -> LoraValue {
let fq = name.to_ascii_lowercase();
match fq.as_str() {
"id" => {
if let Some(LoraValue::Node(id)) = args.first() {
LoraValue::Int(*id as i64)
} else if let Some(LoraValue::Relationship(id)) = args.first() {
LoraValue::Int(*id as i64)
} else {
LoraValue::Null
}
}
"tolower" => match args.first() {
Some(LoraValue::String(s)) => LoraValue::String(s.to_ascii_lowercase()),
_ => LoraValue::Null,
},
"toupper" => match args.first() {
Some(LoraValue::String(s)) => LoraValue::String(s.to_ascii_uppercase()),
_ => LoraValue::Null,
},
"coalesce" => {
for arg in args {
if !matches!(arg, LoraValue::Null) {
return arg.clone();
}
}
LoraValue::Null
}
"type" => match args.first() {
Some(LoraValue::Relationship(id)) => ctx
.storage
.with_relationship(*id, |r| LoraValue::String(r.rel_type.clone()))
.unwrap_or(LoraValue::Null),
_ => LoraValue::Null,
},
"labels" => match args.first() {
Some(LoraValue::Node(id)) => ctx
.storage
.with_node(*id, |n| {
LoraValue::List(
n.labels
.iter()
.map(|s| LoraValue::String(s.clone()))
.collect(),
)
})
.unwrap_or(LoraValue::Null),
_ => LoraValue::Null,
},
"keys" => match args.first() {
Some(LoraValue::Node(id)) => ctx
.storage
.with_node(*id, |n| {
LoraValue::List(
n.properties
.keys()
.map(|k| LoraValue::String(k.clone()))
.collect(),
)
})
.unwrap_or(LoraValue::Null),
Some(LoraValue::Relationship(id)) => ctx
.storage
.with_relationship(*id, |r| {
LoraValue::List(
r.properties
.keys()
.map(|k| LoraValue::String(k.clone()))
.collect(),
)
})
.unwrap_or(LoraValue::Null),
Some(LoraValue::Map(m)) => {
LoraValue::List(m.keys().cloned().map(LoraValue::String).collect())
}
_ => LoraValue::Null,
},
"size" | "length" => match args.first() {
Some(LoraValue::List(l)) => LoraValue::Int(l.len() as i64),
Some(LoraValue::String(s)) => LoraValue::Int(s.len() as i64),
Some(LoraValue::Path(p)) => LoraValue::Int(p.rels.len() as i64),
Some(LoraValue::Vector(v)) => LoraValue::Int(v.dimension as i64),
_ => LoraValue::Null,
},
"nodes" => match args.first() {
Some(LoraValue::Path(p)) => {
LoraValue::List(p.nodes.iter().map(|id| LoraValue::Node(*id)).collect())
}
_ => LoraValue::Null,
},
"relationships" => match args.first() {
Some(LoraValue::Path(p)) => LoraValue::List(
p.rels
.iter()
.map(|id| LoraValue::Relationship(*id))
.collect(),
),
_ => LoraValue::Null,
},
"head" => match args.first() {
Some(LoraValue::List(l)) => l.first().cloned().unwrap_or(LoraValue::Null),
_ => LoraValue::Null,
},
"tail" => match args.first() {
Some(LoraValue::List(l)) => {
if l.is_empty() {
LoraValue::Null
} else {
LoraValue::List(l[1..].to_vec())
}
}
_ => LoraValue::Null,
},
"tostring" => match args.first() {
Some(LoraValue::Int(i)) => LoraValue::String(i.to_string()),
Some(LoraValue::Float(f)) => LoraValue::String(f.to_string()),
Some(LoraValue::Bool(b)) => LoraValue::String(b.to_string()),
Some(LoraValue::String(s)) => LoraValue::String(s.clone()),
Some(LoraValue::Null) => LoraValue::Null,
Some(LoraValue::Date(d)) => LoraValue::String(d.to_string()),
Some(LoraValue::DateTime(dt)) => LoraValue::String(dt.to_string()),
Some(LoraValue::LocalDateTime(dt)) => LoraValue::String(dt.to_string()),
Some(LoraValue::Time(t)) => LoraValue::String(t.to_string()),
Some(LoraValue::LocalTime(t)) => LoraValue::String(t.to_string()),
Some(LoraValue::Duration(dur)) => LoraValue::String(dur.to_string()),
_ => LoraValue::Null,
},
"tointeger" | "toint" => match args.first() {
Some(LoraValue::Int(i)) => LoraValue::Int(*i),
Some(LoraValue::Float(f)) => LoraValue::Int(*f as i64),
Some(LoraValue::String(s)) => s
.parse::<i64>()
.ok()
.map(LoraValue::Int)
.unwrap_or(LoraValue::Null),
_ => LoraValue::Null,
},
"tofloat" => match args.first() {
Some(LoraValue::Float(f)) => LoraValue::Float(*f),
Some(LoraValue::Int(i)) => LoraValue::Float(*i as f64),
Some(LoraValue::String(s)) => s
.parse::<f64>()
.ok()
.map(LoraValue::Float)
.unwrap_or(LoraValue::Null),
_ => LoraValue::Null,
},
"abs" => match args.first() {
Some(LoraValue::Int(i)) => LoraValue::Int(i.abs()),
Some(LoraValue::Float(f)) => LoraValue::Float(f.abs()),
_ => LoraValue::Null,
},
"ceil" => match args.first() {
Some(LoraValue::Float(f)) => LoraValue::Int(f.ceil() as i64),
Some(LoraValue::Int(i)) => LoraValue::Int(*i),
_ => LoraValue::Null,
},
"floor" => match args.first() {
Some(LoraValue::Float(f)) => LoraValue::Int(f.floor() as i64),
Some(LoraValue::Int(i)) => LoraValue::Int(*i),
_ => LoraValue::Null,
},
"round" => match args.first() {
Some(LoraValue::Float(f)) => LoraValue::Int(f.round() as i64),
Some(LoraValue::Int(i)) => LoraValue::Int(*i),
_ => LoraValue::Null,
},
"sqrt" => match args.first() {
Some(LoraValue::Float(f)) => {
if *f < 0.0 {
LoraValue::Null
} else {
LoraValue::Float(f.sqrt())
}
}
Some(LoraValue::Int(i)) => {
if *i < 0 {
LoraValue::Null
} else {
LoraValue::Float((*i as f64).sqrt())
}
}
_ => LoraValue::Null,
},
"sign" => match args.first() {
Some(LoraValue::Int(i)) => LoraValue::Int(i.signum()),
Some(LoraValue::Float(f)) => {
if f.is_nan() {
LoraValue::Null
} else {
LoraValue::Int(f.signum() as i64)
}
}
_ => LoraValue::Null,
},
"trim" => match args.first() {
Some(LoraValue::String(s)) => LoraValue::String(s.trim().to_string()),
_ => LoraValue::Null,
},
"ltrim" => match args.first() {
Some(LoraValue::String(s)) => LoraValue::String(s.trim_start().to_string()),
_ => LoraValue::Null,
},
"rtrim" => match args.first() {
Some(LoraValue::String(s)) => LoraValue::String(s.trim_end().to_string()),
_ => LoraValue::Null,
},
"replace" => match (args.first(), args.get(1), args.get(2)) {
(
Some(LoraValue::String(s)),
Some(LoraValue::String(search)),
Some(LoraValue::String(replacement)),
) => LoraValue::String(s.replace(search.as_str(), replacement.as_str())),
_ => LoraValue::Null,
},
"split" => match (args.first(), args.get(1)) {
(Some(LoraValue::String(s)), Some(LoraValue::String(delimiter))) => LoraValue::List(
s.split(delimiter.as_str())
.map(|part| LoraValue::String(part.to_string()))
.collect(),
),
_ => LoraValue::Null,
},
"substring" => {
match args.first() {
Some(LoraValue::String(s)) => {
let start = args.get(1).and_then(|v| v.as_i64()).unwrap_or(0) as usize;
if start > s.len() {
return LoraValue::String(String::new());
}
match args.get(2).and_then(|v| v.as_i64()) {
Some(len) => {
let len = len.max(0) as usize;
let end = (start + len).min(s.len());
LoraValue::String(s[start..end].to_string())
}
None => {
LoraValue::String(s[start..].to_string())
}
}
}
_ => LoraValue::Null,
}
}
"reverse" => match args.first() {
Some(LoraValue::String(s)) => LoraValue::String(s.chars().rev().collect()),
Some(LoraValue::List(l)) => LoraValue::List(l.iter().rev().cloned().collect()),
_ => LoraValue::Null,
},
"left" => match (args.first(), args.get(1)) {
(Some(LoraValue::String(s)), Some(LoraValue::Int(n))) => {
let n = (*n).max(0) as usize;
LoraValue::String(s.chars().take(n).collect())
}
_ => LoraValue::Null,
},
"right" => match (args.first(), args.get(1)) {
(Some(LoraValue::String(s)), Some(LoraValue::Int(n))) => {
let n = (*n).max(0) as usize;
let char_count = s.chars().count();
let skip = char_count.saturating_sub(n);
LoraValue::String(s.chars().skip(skip).collect())
}
_ => LoraValue::Null,
},
"properties" => match args.first() {
Some(LoraValue::Node(id)) => ctx
.storage
.with_node(*id, |n| {
LoraValue::Map(
n.properties
.iter()
.map(|(k, v)| (k.clone(), LoraValue::from(v)))
.collect(),
)
})
.unwrap_or(LoraValue::Null),
Some(LoraValue::Relationship(id)) => ctx
.storage
.with_relationship(*id, |r| {
LoraValue::Map(
r.properties
.iter()
.map(|(k, v)| (k.clone(), LoraValue::from(v)))
.collect(),
)
})
.unwrap_or(LoraValue::Null),
Some(LoraValue::Map(m)) => LoraValue::Map(m.clone()),
_ => LoraValue::Null,
},
"timestamp" => {
use std::time::{SystemTime, UNIX_EPOCH};
let millis = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0);
LoraValue::Int(millis)
}
"range" => {
let start = args.first().and_then(|v| v.as_i64()).unwrap_or(0);
let end = args.get(1).and_then(|v| v.as_i64()).unwrap_or(0);
let step = args.get(2).and_then(|v| v.as_i64()).unwrap_or(1);
if step == 0 {
return LoraValue::Null;
}
let mut result = Vec::new();
let mut i = start;
if step > 0 {
while i <= end {
result.push(LoraValue::Int(i));
i += step;
}
} else {
while i >= end {
result.push(LoraValue::Int(i));
i += step;
}
}
LoraValue::List(result)
}
"last" => match args.first() {
Some(LoraValue::List(l)) => l.last().cloned().unwrap_or(LoraValue::Null),
_ => LoraValue::Null,
},
"lpad" => match (args.first(), args.get(1), args.get(2)) {
(Some(LoraValue::String(s)), Some(len_val), Some(LoraValue::String(pad))) => {
let target_len = len_val.as_i64().unwrap_or(0).max(0) as usize;
let current_len = s.chars().count();
if current_len >= target_len {
LoraValue::String(s.clone())
} else {
let pad_needed = target_len - current_len;
let pad_chars: String = pad.chars().cycle().take(pad_needed).collect();
LoraValue::String(format!("{}{}", pad_chars, s))
}
}
_ => LoraValue::Null,
},
"rpad" => match (args.first(), args.get(1), args.get(2)) {
(Some(LoraValue::String(s)), Some(len_val), Some(LoraValue::String(pad))) => {
let target_len = len_val.as_i64().unwrap_or(0).max(0) as usize;
let current_len = s.chars().count();
if current_len >= target_len {
LoraValue::String(s.clone())
} else {
let pad_needed = target_len - current_len;
let pad_chars: String = pad.chars().cycle().take(pad_needed).collect();
LoraValue::String(format!("{}{}", s, pad_chars))
}
}
_ => LoraValue::Null,
},
"char_length" => match args.first() {
Some(LoraValue::String(s)) => LoraValue::Int(s.chars().count() as i64),
_ => LoraValue::Null,
},
"normalize" => match args.first() {
Some(LoraValue::String(s)) => LoraValue::String(s.clone()),
_ => LoraValue::Null,
},
"toboolean" | "tobooleanornull" => match args.first() {
Some(LoraValue::Bool(b)) => LoraValue::Bool(*b),
Some(LoraValue::String(s)) => match s.to_ascii_lowercase().as_str() {
"true" => LoraValue::Bool(true),
"false" => LoraValue::Bool(false),
_ => LoraValue::Null,
},
Some(LoraValue::Int(i)) => match *i {
0 => LoraValue::Bool(false),
_ => LoraValue::Bool(true),
},
Some(LoraValue::Null) => LoraValue::Null,
_ => LoraValue::Null,
},
"valuetype" => match args.first() {
Some(LoraValue::Null) => LoraValue::String("NULL".to_string()),
Some(LoraValue::Bool(_)) => LoraValue::String("BOOLEAN".to_string()),
Some(LoraValue::Int(_)) => LoraValue::String("INTEGER".to_string()),
Some(LoraValue::Float(_)) => LoraValue::String("FLOAT".to_string()),
Some(LoraValue::String(_)) => LoraValue::String("STRING".to_string()),
Some(LoraValue::List(items)) => {
let elem_type = if items.is_empty() {
"ANY"
} else {
let first_type = match &items[0] {
LoraValue::Int(_) => "INTEGER",
LoraValue::Float(_) => "FLOAT",
LoraValue::String(_) => "STRING",
LoraValue::Bool(_) => "BOOLEAN",
LoraValue::Null => "ANY",
_ => "ANY",
};
let homogeneous = items.iter().all(|v| {
matches!(
(v, first_type),
(LoraValue::Int(_), "INTEGER")
| (LoraValue::Float(_), "FLOAT")
| (LoraValue::String(_), "STRING")
| (LoraValue::Bool(_), "BOOLEAN")
)
});
if homogeneous {
first_type
} else {
"ANY"
}
};
LoraValue::String(format!("LIST<{elem_type}>"))
}
Some(LoraValue::Map(_)) => LoraValue::String("MAP".to_string()),
Some(LoraValue::Node(_)) => LoraValue::String("NODE".to_string()),
Some(LoraValue::Relationship(_)) => LoraValue::String("RELATIONSHIP".to_string()),
Some(LoraValue::Path(_)) => LoraValue::String("PATH".to_string()),
Some(LoraValue::Date(_)) => LoraValue::String("DATE".to_string()),
Some(LoraValue::DateTime(_)) => LoraValue::String("DATE_TIME".to_string()),
Some(LoraValue::LocalDateTime(_)) => LoraValue::String("LOCAL_DATE_TIME".to_string()),
Some(LoraValue::Time(_)) => LoraValue::String("TIME".to_string()),
Some(LoraValue::LocalTime(_)) => LoraValue::String("LOCAL_TIME".to_string()),
Some(LoraValue::Duration(_)) => LoraValue::String("DURATION".to_string()),
Some(LoraValue::Point(_)) => LoraValue::String("POINT".to_string()),
Some(LoraValue::Vector(v)) => LoraValue::String(format!(
"VECTOR<{}>({})",
v.coordinate_type().as_str(),
v.dimension
)),
None => LoraValue::Null,
},
"log" | "ln" => match args.first() {
Some(v) => match v.as_f64() {
Some(f) if f > 0.0 => LoraValue::Float(f.ln()),
_ => LoraValue::Null,
},
_ => LoraValue::Null,
},
"log10" => match args.first() {
Some(v) => match v.as_f64() {
Some(f) if f > 0.0 => LoraValue::Float(f.log10()),
_ => LoraValue::Null,
},
_ => LoraValue::Null,
},
"exp" => match args.first() {
Some(v) => match v.as_f64() {
Some(f) => LoraValue::Float(f.exp()),
None => LoraValue::Null,
},
_ => LoraValue::Null,
},
"sin" => match args.first() {
Some(v) => match v.as_f64() {
Some(f) => LoraValue::Float(f.sin()),
None => LoraValue::Null,
},
_ => LoraValue::Null,
},
"cos" => match args.first() {
Some(v) => match v.as_f64() {
Some(f) => LoraValue::Float(f.cos()),
None => LoraValue::Null,
},
_ => LoraValue::Null,
},
"tan" => match args.first() {
Some(v) => match v.as_f64() {
Some(f) => LoraValue::Float(f.tan()),
None => LoraValue::Null,
},
_ => LoraValue::Null,
},
"asin" => match args.first() {
Some(v) => match v.as_f64() {
Some(f) if (-1.0..=1.0).contains(&f) => LoraValue::Float(f.asin()),
_ => LoraValue::Null,
},
_ => LoraValue::Null,
},
"acos" => match args.first() {
Some(v) => match v.as_f64() {
Some(f) if (-1.0..=1.0).contains(&f) => LoraValue::Float(f.acos()),
_ => LoraValue::Null,
},
_ => LoraValue::Null,
},
"atan" => match args.first() {
Some(v) => match v.as_f64() {
Some(f) => LoraValue::Float(f.atan()),
None => LoraValue::Null,
},
_ => LoraValue::Null,
},
"atan2" => match (args.first(), args.get(1)) {
(Some(y_val), Some(x_val)) => match (y_val.as_f64(), x_val.as_f64()) {
(Some(y), Some(x)) => LoraValue::Float(y.atan2(x)),
_ => LoraValue::Null,
},
_ => LoraValue::Null,
},
"pi" => LoraValue::Float(std::f64::consts::PI),
"e" => LoraValue::Float(std::f64::consts::E),
"rand" => {
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
let hash = ((nanos as u64)
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407)) as f64;
LoraValue::Float((hash / u64::MAX as f64).abs())
}
"degrees" => match args.first() {
Some(v) => match v.as_f64() {
Some(f) => LoraValue::Float(f.to_degrees()),
None => LoraValue::Null,
},
_ => LoraValue::Null,
},
"radians" => match args.first() {
Some(v) => match v.as_f64() {
Some(f) => LoraValue::Float(f.to_radians()),
None => LoraValue::Null,
},
_ => LoraValue::Null,
},
"date" => match args.first() {
None => LoraValue::Date(LoraDate::today()),
Some(LoraValue::String(s)) => match LoraDate::parse(s) {
Ok(d) => LoraValue::Date(d),
Err(e) => {
set_eval_error(e);
LoraValue::Null
}
},
Some(LoraValue::Map(m)) => {
let year = m.get("year").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
let month = m.get("month").and_then(|v| v.as_i64()).unwrap_or(1) as u32;
let day = m.get("day").and_then(|v| v.as_i64()).unwrap_or(1) as u32;
match LoraDate::new(year, month, day) {
Ok(d) => LoraValue::Date(d),
Err(e) => {
set_eval_error(e);
LoraValue::Null
}
}
}
Some(LoraValue::Date(d)) => LoraValue::Date(d.clone()),
_ => LoraValue::Null,
},
"datetime" => match args.first() {
None => LoraValue::DateTime(LoraDateTime::now()),
Some(LoraValue::String(s)) => match LoraDateTime::parse(s) {
Ok(dt) => LoraValue::DateTime(dt),
Err(e) => {
set_eval_error(e);
LoraValue::Null
}
},
Some(LoraValue::Map(m)) => {
let year = m.get("year").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
let month = m.get("month").and_then(|v| v.as_i64()).unwrap_or(1) as u32;
let day = m.get("day").and_then(|v| v.as_i64()).unwrap_or(1) as u32;
let hour = m.get("hour").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
let minute = m.get("minute").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
let second = m.get("second").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
let ms = m.get("millisecond").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
let offset = if let Some(LoraValue::String(tz)) = m.get("timezone") {
timezone_name_to_offset(tz)
} else {
0
};
match LoraDateTime::new(
year,
month,
day,
hour,
minute,
second,
ms * 1_000_000,
offset,
) {
Ok(dt) => LoraValue::DateTime(dt),
Err(e) => {
set_eval_error(e);
LoraValue::Null
}
}
}
_ => LoraValue::Null,
},
"time" => match args.first() {
None => LoraValue::Time(LoraTime::now()),
Some(LoraValue::String(s)) => match LoraTime::parse(s) {
Ok(t) => LoraValue::Time(t),
Err(e) => {
set_eval_error(e);
LoraValue::Null
}
},
_ => LoraValue::Null,
},
"localtime" => match args.first() {
None => LoraValue::LocalTime(LoraLocalTime::now()),
Some(LoraValue::String(s)) => match LoraLocalTime::parse(s) {
Ok(t) => LoraValue::LocalTime(t),
Err(e) => {
set_eval_error(e);
LoraValue::Null
}
},
_ => LoraValue::Null,
},
"localdatetime" => match args.first() {
None => LoraValue::LocalDateTime(LoraLocalDateTime::now()),
Some(LoraValue::String(s)) => match LoraLocalDateTime::parse(s) {
Ok(dt) => LoraValue::LocalDateTime(dt),
Err(e) => {
set_eval_error(e);
LoraValue::Null
}
},
_ => LoraValue::Null,
},
"duration" => match args.first() {
Some(LoraValue::String(s)) => match LoraDuration::parse(s) {
Ok(d) => LoraValue::Duration(d),
Err(e) => {
set_eval_error(e);
LoraValue::Null
}
},
Some(LoraValue::Map(m)) => {
let years = m.get("years").and_then(|v| v.as_i64()).unwrap_or(0);
let months = m.get("months").and_then(|v| v.as_i64()).unwrap_or(0);
let days = m.get("days").and_then(|v| v.as_i64()).unwrap_or(0);
let hours = m.get("hours").and_then(|v| v.as_i64()).unwrap_or(0);
let minutes = m.get("minutes").and_then(|v| v.as_i64()).unwrap_or(0);
let seconds = m.get("seconds").and_then(|v| v.as_i64()).unwrap_or(0);
LoraValue::Duration(LoraDuration {
months: years * 12 + months,
days,
seconds: hours * 3600 + minutes * 60 + seconds,
nanoseconds: 0,
})
}
_ => LoraValue::Null,
},
"date.truncate" => match (args.first(), args.get(1)) {
(Some(LoraValue::String(unit)), Some(LoraValue::Date(d))) => match unit.as_str() {
"month" => LoraValue::Date(d.truncate_to_month()),
"year" => LoraValue::Date(LoraDate {
year: d.year,
month: 1,
day: 1,
}),
_ => LoraValue::Date(d.clone()),
},
_ => LoraValue::Null,
},
"datetime.truncate" => match (args.first(), args.get(1)) {
(Some(LoraValue::String(unit)), Some(LoraValue::DateTime(dt))) => match unit.as_str() {
"day" => LoraValue::DateTime(dt.truncate_to_day()),
"hour" => LoraValue::DateTime(dt.truncate_to_hour()),
"month" => LoraValue::DateTime(LoraDateTime {
year: dt.year,
month: dt.month,
day: 1,
hour: 0,
minute: 0,
second: 0,
nanosecond: 0,
offset_seconds: dt.offset_seconds,
}),
_ => LoraValue::DateTime(dt.clone()),
},
_ => LoraValue::Null,
},
"duration.between" => match (args.first(), args.get(1)) {
(Some(LoraValue::Date(d1)), Some(LoraValue::Date(d2))) => {
LoraValue::Duration(LoraDuration::between_dates(d1, d2))
}
(Some(LoraValue::DateTime(dt1)), Some(LoraValue::DateTime(dt2))) => {
LoraValue::Duration(LoraDuration::between_datetimes(dt1, dt2))
}
_ => LoraValue::Null,
},
"duration.indays" => match (args.first(), args.get(1)) {
(Some(LoraValue::Date(d1)), Some(LoraValue::Date(d2))) => {
LoraValue::Duration(LoraDuration::in_days(d1, d2))
}
_ => LoraValue::Null,
},
"point" => match args.first() {
None | Some(LoraValue::Null) => LoraValue::Null,
Some(LoraValue::Map(m)) => match build_point_from_map(m) {
Ok(Some(p)) => LoraValue::Point(p),
Ok(None) => LoraValue::Null,
Err(msg) => {
set_eval_error(msg);
LoraValue::Null
}
},
Some(_) => {
set_eval_error("point() requires a map argument".to_string());
LoraValue::Null
}
},
"distance" => match (args.first(), args.get(1)) {
(Some(LoraValue::Point(a)), Some(LoraValue::Point(b))) => match point_distance(a, b) {
Some(d) => LoraValue::Float(d),
None => {
set_eval_error(
"Cannot compute distance between points with different SRIDs".to_string(),
);
LoraValue::Null
}
},
_ => LoraValue::Null,
},
"vector" => eval_vector_ctor(args),
"tointegerlist" => match args.first() {
Some(LoraValue::Null) => LoraValue::Null,
Some(LoraValue::Vector(v)) => LoraValue::List(
v.values
.to_i64_vec()
.into_iter()
.map(LoraValue::Int)
.collect(),
),
Some(other) => {
set_eval_error(format!(
"toIntegerList() expected VECTOR, got {}",
crate::errors::value_kind(other)
));
LoraValue::Null
}
None => LoraValue::Null,
},
"tofloatlist" => match args.first() {
Some(LoraValue::Null) => LoraValue::Null,
Some(LoraValue::Vector(v)) => LoraValue::List(
v.values
.as_f64_vec()
.into_iter()
.map(LoraValue::Float)
.collect(),
),
Some(other) => {
set_eval_error(format!(
"toFloatList() expected VECTOR, got {}",
crate::errors::value_kind(other)
));
LoraValue::Null
}
None => LoraValue::Null,
},
"vector_dimension_count" => match args.first() {
Some(LoraValue::Null) => LoraValue::Null,
Some(LoraValue::Vector(v)) => LoraValue::Int(v.dimension as i64),
Some(other) => {
set_eval_error(format!(
"vector_dimension_count() expected VECTOR, got {}",
crate::errors::value_kind(other)
));
LoraValue::Null
}
None => LoraValue::Null,
},
"vector.similarity.cosine" => eval_vector_sim_cosine(args),
"vector.similarity.euclidean" => eval_vector_sim_euclidean(args),
"vector_distance" => eval_vector_distance_fn(args),
"vector_norm" => eval_vector_norm_fn(args),
_ => LoraValue::Null,
}
}
fn coerce_list_to_raw_coords(items: &[LoraValue]) -> Result<Vec<RawCoordinate>, String> {
let mut out = Vec::with_capacity(items.len());
for item in items {
match item {
LoraValue::Int(i) => out.push(RawCoordinate::Int(*i)),
LoraValue::Float(f) => {
if !f.is_finite() {
return Err("vector coordinates cannot be NaN or Infinity".to_string());
}
out.push(RawCoordinate::Float(*f));
}
LoraValue::List(_) => {
return Err("vector coordinates cannot contain nested lists".to_string());
}
LoraValue::Null => {
return Err("vector coordinates cannot be null".to_string());
}
other => {
return Err(format!(
"vector coordinates must be numeric, got {}",
crate::errors::value_kind(other)
));
}
}
}
Ok(out)
}
fn eval_vector_ctor(args: &[LoraValue]) -> LoraValue {
let value = args.first();
let dimension = args.get(1);
let type_arg = args.get(2);
if matches!(value, Some(LoraValue::Null)) || matches!(dimension, Some(LoraValue::Null)) {
return LoraValue::Null;
}
let Some(type_val) = type_arg else {
return LoraValue::Null;
};
let type_name = match type_val {
LoraValue::String(s) => s.clone(),
LoraValue::Null => {
set_eval_error("vector() coordinateType must not be null".to_string());
return LoraValue::Null;
}
other => {
set_eval_error(format!(
"vector() coordinateType must be a string or type literal, got {}",
crate::errors::value_kind(other)
));
return LoraValue::Null;
}
};
let Some(coordinate_type) = VectorCoordinateType::parse(&type_name) else {
set_eval_error(format!("unknown vector coordinate type '{type_name}'"));
return LoraValue::Null;
};
let dim_i64 = match dimension {
Some(LoraValue::Int(i)) => *i,
Some(LoraValue::Float(f)) if f.fract() == 0.0 => *f as i64,
Some(other) => {
set_eval_error(format!(
"vector() dimension must be INTEGER, got {}",
crate::errors::value_kind(other)
));
return LoraValue::Null;
}
None => {
set_eval_error("vector() requires a dimension argument".to_string());
return LoraValue::Null;
}
};
let raw = match value {
Some(LoraValue::List(items)) => match coerce_list_to_raw_coords(items) {
Ok(r) => r,
Err(e) => {
set_eval_error(e);
return LoraValue::Null;
}
},
Some(LoraValue::String(s)) => match parse_string_values(s) {
Ok(r) => r,
Err(e) => {
set_eval_error(e.to_string());
return LoraValue::Null;
}
},
Some(other) => {
set_eval_error(format!(
"vector() value must be LIST<NUMBER> or STRING, got {}",
crate::errors::value_kind(other)
));
return LoraValue::Null;
}
None => {
set_eval_error("vector() requires a value argument".to_string());
return LoraValue::Null;
}
};
match LoraVector::try_new(raw, dim_i64, coordinate_type) {
Ok(v) => LoraValue::Vector(v),
Err(e) => {
set_eval_error(e.to_string());
LoraValue::Null
}
}
}
fn coerce_similarity_input(value: &LoraValue) -> Result<Option<LoraVector>, String> {
match value {
LoraValue::Null => Ok(None),
LoraValue::Vector(v) => Ok(Some(v.clone())),
LoraValue::List(items) => {
let raw = coerce_list_to_raw_coords(items)?;
let dim = raw.len() as i64;
if dim == 0 {
return Err("vector similarity cannot be computed on an empty list".to_string());
}
LoraVector::try_new(raw, dim, VectorCoordinateType::Float32)
.map(Some)
.map_err(|e| e.to_string())
}
other => Err(format!(
"expected VECTOR or LIST<NUMBER>, got {}",
crate::errors::value_kind(other)
)),
}
}
fn eval_vector_sim_cosine(args: &[LoraValue]) -> LoraValue {
let a = args.first();
let b = args.get(1);
let (Some(a), Some(b)) = (a, b) else {
return LoraValue::Null;
};
let av = match coerce_similarity_input(a) {
Ok(Some(v)) => v,
Ok(None) => return LoraValue::Null,
Err(e) => {
set_eval_error(e);
return LoraValue::Null;
}
};
let bv = match coerce_similarity_input(b) {
Ok(Some(v)) => v,
Ok(None) => return LoraValue::Null,
Err(e) => {
set_eval_error(e);
return LoraValue::Null;
}
};
if av.dimension != bv.dimension {
set_eval_error(format!(
"vector.similarity.cosine requires equal dimensions, got {} and {}",
av.dimension, bv.dimension
));
return LoraValue::Null;
}
match cosine_similarity_bounded(&av, &bv) {
Some(s) => LoraValue::Float(s),
None => LoraValue::Null,
}
}
fn eval_vector_sim_euclidean(args: &[LoraValue]) -> LoraValue {
let a = args.first();
let b = args.get(1);
let (Some(a), Some(b)) = (a, b) else {
return LoraValue::Null;
};
let av = match coerce_similarity_input(a) {
Ok(Some(v)) => v,
Ok(None) => return LoraValue::Null,
Err(e) => {
set_eval_error(e);
return LoraValue::Null;
}
};
let bv = match coerce_similarity_input(b) {
Ok(Some(v)) => v,
Ok(None) => return LoraValue::Null,
Err(e) => {
set_eval_error(e);
return LoraValue::Null;
}
};
if av.dimension != bv.dimension {
set_eval_error(format!(
"vector.similarity.euclidean requires equal dimensions, got {} and {}",
av.dimension, bv.dimension
));
return LoraValue::Null;
}
match euclidean_similarity(&av, &bv) {
Some(s) => LoraValue::Float(s),
None => LoraValue::Null,
}
}
fn eval_vector_distance_fn(args: &[LoraValue]) -> LoraValue {
let a = args.first();
let b = args.get(1);
let metric = args.get(2);
if matches!(a, Some(LoraValue::Null)) || matches!(b, Some(LoraValue::Null)) {
return LoraValue::Null;
}
let (Some(LoraValue::Vector(av)), Some(LoraValue::Vector(bv))) = (a, b) else {
set_eval_error("vector_distance() requires two VECTOR arguments".to_string());
return LoraValue::Null;
};
if av.dimension != bv.dimension {
set_eval_error(format!(
"vector_distance() requires equal dimensions, got {} and {}",
av.dimension, bv.dimension
));
return LoraValue::Null;
}
let metric_str = match metric {
Some(LoraValue::String(s)) => s.clone(),
Some(LoraValue::Null) => return LoraValue::Null,
_ => {
set_eval_error("vector_distance() metric must be a string/identifier".to_string());
return LoraValue::Null;
}
};
let result = match metric_str.to_ascii_uppercase().as_str() {
"EUCLIDEAN" => euclidean_distance(av, bv),
"EUCLIDEAN_SQUARED" => euclidean_distance_squared(av, bv),
"MANHATTAN" => manhattan_distance(av, bv),
"COSINE" => cosine_similarity_raw(av, bv).map(|s| 1.0 - s),
"DOT" => dot_product(av, bv).map(|d| -d),
"HAMMING" => hamming_distance(av, bv),
other => {
set_eval_error(format!("unknown vector distance metric '{other}'"));
return LoraValue::Null;
}
};
result.map(LoraValue::Float).unwrap_or(LoraValue::Null)
}
fn eval_vector_norm_fn(args: &[LoraValue]) -> LoraValue {
let v = args.first();
let metric = args.get(1);
if matches!(v, Some(LoraValue::Null)) {
return LoraValue::Null;
}
let Some(LoraValue::Vector(v)) = v else {
set_eval_error("vector_norm() requires a VECTOR argument".to_string());
return LoraValue::Null;
};
let metric_str = match metric {
Some(LoraValue::String(s)) => s.clone(),
Some(LoraValue::Null) => return LoraValue::Null,
_ => {
set_eval_error("vector_norm() metric must be a string/identifier".to_string());
return LoraValue::Null;
}
};
match metric_str.to_ascii_uppercase().as_str() {
"EUCLIDEAN" => LoraValue::Float(euclidean_norm(v)),
"MANHATTAN" => LoraValue::Float(manhattan_norm(v)),
other => {
set_eval_error(format!("unknown vector norm metric '{other}'"));
LoraValue::Null
}
}
}
thread_local! {
static EVAL_ERROR: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
}
fn set_eval_error(msg: String) {
EVAL_ERROR.with(|e| *e.borrow_mut() = Some(msg));
}
pub fn take_eval_error() -> Option<String> {
EVAL_ERROR.with(|e| e.borrow_mut().take())
}
fn build_point_from_map(map: &BTreeMap<String, LoraValue>) -> Result<Option<LoraPoint>, String> {
const KNOWN_KEYS: &[&str] = &[
"x",
"y",
"z",
"longitude",
"latitude",
"height",
"crs",
"srid",
];
for k in map.keys() {
if !KNOWN_KEYS.iter().any(|known| known.eq_ignore_ascii_case(k)) {
return Err(format!("point() got unknown key '{k}'"));
}
}
let x = take_numeric(map, "x")?;
let y = take_numeric(map, "y")?;
let z = take_numeric(map, "z")?;
let longitude = take_numeric(map, "longitude")?;
let latitude = take_numeric(map, "latitude")?;
let height = take_numeric(map, "height")?;
let crs = take_string(map, "crs")?;
let srid = take_integer(map, "srid")?;
if matches!(x, Some(None))
|| matches!(y, Some(None))
|| matches!(z, Some(None))
|| matches!(longitude, Some(None))
|| matches!(latitude, Some(None))
|| matches!(height, Some(None))
|| matches!(crs, Some(None))
|| matches!(srid, Some(None))
{
return Ok(None);
}
let x = x.and_then(|v| v);
let y = y.and_then(|v| v);
let z = z.and_then(|v| v);
let longitude = longitude.and_then(|v| v);
let latitude = latitude.and_then(|v| v);
let height = height.and_then(|v| v);
let crs = crs.and_then(|v| v);
let srid = srid.and_then(|v| v);
let has_cartesian = x.is_some() || y.is_some();
let has_geographic = longitude.is_some() || latitude.is_some();
if has_cartesian && has_geographic {
return Err(
"point() cannot mix cartesian (x/y) and geographic (longitude/latitude) keys"
.to_string(),
);
}
let (family, first, second) = if has_geographic {
(
PointKeyFamily::Geographic,
longitude.ok_or_else(|| "point() is missing longitude".to_string())?,
latitude.ok_or_else(|| "point() is missing latitude".to_string())?,
)
} else if has_cartesian {
(
PointKeyFamily::Cartesian,
x.ok_or_else(|| "point() is missing x".to_string())?,
y.ok_or_else(|| "point() is missing y".to_string())?,
)
} else {
return Err(
"point() requires coordinates — either {x, y} or {longitude, latitude}".to_string(),
);
};
let third = match (z, height) {
(Some(_), Some(_)) => {
return Err(
"point() cannot specify both 'z' and 'height' — they are aliases".to_string(),
);
}
(Some(v), None) | (None, Some(v)) => Some(v),
(None, None) => None,
};
let is_3d = third.is_some();
let final_srid = resolve_srid(crs.as_deref(), srid, family, is_3d)?;
let point = LoraPoint {
x: first,
y: second,
z: if srid_is_3d(final_srid) { third } else { None },
srid: final_srid,
};
Ok(Some(point))
}
fn take_numeric(
map: &BTreeMap<String, LoraValue>,
key: &str,
) -> Result<Option<Option<f64>>, String> {
match map.get(key) {
None => Ok(None),
Some(LoraValue::Null) => Ok(Some(None)),
Some(LoraValue::Int(v)) => Ok(Some(Some(*v as f64))),
Some(LoraValue::Float(v)) => Ok(Some(Some(*v))),
Some(other) => Err(format!(
"point() field '{key}' must be numeric, got {}",
crate::errors::value_kind(other)
)),
}
}
fn take_string(
map: &BTreeMap<String, LoraValue>,
key: &str,
) -> Result<Option<Option<String>>, String> {
match map.get(key) {
None => Ok(None),
Some(LoraValue::Null) => Ok(Some(None)),
Some(LoraValue::String(s)) => Ok(Some(Some(s.clone()))),
Some(other) => Err(format!(
"point() field '{key}' must be a string, got {}",
crate::errors::value_kind(other)
)),
}
}
fn take_integer(
map: &BTreeMap<String, LoraValue>,
key: &str,
) -> Result<Option<Option<i64>>, String> {
match map.get(key) {
None => Ok(None),
Some(LoraValue::Null) => Ok(Some(None)),
Some(LoraValue::Int(v)) => Ok(Some(Some(*v))),
Some(other) => Err(format!(
"point() field '{key}' must be an integer, got {}",
crate::errors::value_kind(other)
)),
}
}
fn timezone_name_to_offset(name: &str) -> i32 {
match name {
"UTC" | "GMT" | "Z" => 0,
"Europe/London" => 0, "Europe/Amsterdam" | "Europe/Berlin" | "Europe/Paris" | "CET" => 3600, "Europe/Moscow" => 10800, "US/Eastern" | "America/New_York" | "EST" => -18000, "US/Central" | "America/Chicago" | "CST" => -21600, "US/Mountain" | "America/Denver" | "MST" => -25200, "US/Pacific" | "America/Los_Angeles" | "PST" => -28800, "Asia/Tokyo" | "JST" => 32400, "Asia/Shanghai" | "Asia/Hong_Kong" => 28800, _ => 0, }
}
fn value_eq(a: &LoraValue, b: &LoraValue) -> bool {
match (a, b) {
(LoraValue::Null, LoraValue::Null) => true,
(LoraValue::Bool(x), LoraValue::Bool(y)) => x == y,
(LoraValue::Int(x), LoraValue::Int(y)) => x == y,
(LoraValue::Float(x), LoraValue::Float(y)) => x == y,
(LoraValue::Int(x), LoraValue::Float(y)) => (*x as f64) == *y,
(LoraValue::Float(x), LoraValue::Int(y)) => *x == (*y as f64),
(LoraValue::String(x), LoraValue::String(y)) => x == y,
(LoraValue::Node(x), LoraValue::Node(y)) => x == y,
(LoraValue::Relationship(x), LoraValue::Relationship(y)) => x == y,
(LoraValue::List(x), LoraValue::List(y)) => x == y,
(LoraValue::Map(x), LoraValue::Map(y)) => x == y,
(LoraValue::Date(x), LoraValue::Date(y)) => x == y,
(LoraValue::DateTime(x), LoraValue::DateTime(y)) => x == y,
(LoraValue::LocalDateTime(x), LoraValue::LocalDateTime(y)) => x == y,
(LoraValue::Time(x), LoraValue::Time(y)) => x == y,
(LoraValue::LocalTime(x), LoraValue::LocalTime(y)) => x == y,
(LoraValue::Duration(x), LoraValue::Duration(y)) => x == y,
(LoraValue::Point(x), LoraValue::Point(y)) => x == y,
(LoraValue::Vector(x), LoraValue::Vector(y)) => x == y,
_ => false,
}
}
fn cmp_numeric_or_string(
lhs: LoraValue,
rhs: LoraValue,
num_cmp: impl Fn(f64, f64) -> bool,
str_cmp: impl Fn(&str, &str) -> bool,
) -> LoraValue {
match (&lhs, &rhs) {
(LoraValue::String(a), LoraValue::String(b)) => LoraValue::Bool(str_cmp(a, b)),
(LoraValue::Date(a), LoraValue::Date(b)) => {
LoraValue::Bool(num_cmp(a.to_epoch_days() as f64, b.to_epoch_days() as f64))
}
(LoraValue::DateTime(a), LoraValue::DateTime(b)) => LoraValue::Bool(num_cmp(
a.to_epoch_millis() as f64,
b.to_epoch_millis() as f64,
)),
(LoraValue::Duration(a), LoraValue::Duration(b)) => {
LoraValue::Bool(num_cmp(a.total_seconds_approx(), b.total_seconds_approx()))
}
_ => match (lhs.as_f64(), rhs.as_f64()) {
(Some(a), Some(b)) => LoraValue::Bool(num_cmp(a, b)),
_ => LoraValue::Bool(false),
},
}
}
fn add_values(lhs: LoraValue, rhs: LoraValue) -> LoraValue {
match (lhs, rhs) {
(LoraValue::Int(a), LoraValue::Int(b)) => LoraValue::Int(a + b),
(LoraValue::String(a), LoraValue::String(b)) => LoraValue::String(a + &b),
(LoraValue::List(mut a), LoraValue::List(b)) => {
a.extend(b);
LoraValue::List(a)
}
(LoraValue::Date(d), LoraValue::Duration(dur)) => LoraValue::Date(d.add_duration(&dur)),
(LoraValue::Duration(dur), LoraValue::Date(d)) => LoraValue::Date(d.add_duration(&dur)),
(LoraValue::DateTime(dt), LoraValue::Duration(dur)) => {
LoraValue::DateTime(dt.add_duration(&dur))
}
(LoraValue::Duration(dur), LoraValue::DateTime(dt)) => {
LoraValue::DateTime(dt.add_duration(&dur))
}
(LoraValue::Duration(a), LoraValue::Duration(b)) => LoraValue::Duration(a.add(&b)),
(LoraValue::Date(_), _) | (_, LoraValue::Date(_)) => {
set_eval_error("Cannot add non-duration to date".to_string());
LoraValue::Null
}
(LoraValue::DateTime(_), _) | (_, LoraValue::DateTime(_)) => {
set_eval_error("Cannot add non-duration to datetime".to_string());
LoraValue::Null
}
(a, b) => match (a.as_f64(), b.as_f64()) {
(Some(a), Some(b)) => LoraValue::Float(a + b),
_ => LoraValue::Null,
},
}
}
fn sub_values(lhs: LoraValue, rhs: LoraValue) -> LoraValue {
match (lhs, rhs) {
(LoraValue::Int(a), LoraValue::Int(b)) => LoraValue::Int(a - b),
(LoraValue::Date(d), LoraValue::Duration(dur)) => LoraValue::Date(d.sub_duration(&dur)),
(LoraValue::DateTime(dt), LoraValue::Duration(dur)) => {
LoraValue::DateTime(dt.add_duration(&dur.negate()))
}
(LoraValue::Date(d1), LoraValue::Date(d2)) => {
LoraValue::Duration(LoraDuration::in_days(&d2, &d1))
}
(LoraValue::DateTime(dt1), LoraValue::DateTime(dt2)) => {
LoraValue::Duration(LoraDuration::between_datetimes(&dt2, &dt1))
}
(LoraValue::Duration(a), LoraValue::Duration(b)) => LoraValue::Duration(a.add(&b.negate())),
(a, b) => match (a.as_f64(), b.as_f64()) {
(Some(a), Some(b)) => LoraValue::Float(a - b),
_ => LoraValue::Null,
},
}
}
fn mul_values(lhs: LoraValue, rhs: LoraValue) -> LoraValue {
match (lhs, rhs) {
(LoraValue::Int(a), LoraValue::Int(b)) => LoraValue::Int(a * b),
(LoraValue::Duration(d), LoraValue::Int(n)) => LoraValue::Duration(d.mul_int(n)),
(LoraValue::Int(n), LoraValue::Duration(d)) => LoraValue::Duration(d.mul_int(n)),
(a, b) => match (a.as_f64(), b.as_f64()) {
(Some(a), Some(b)) => LoraValue::Float(a * b),
_ => LoraValue::Null,
},
}
}
fn div_values(lhs: LoraValue, rhs: LoraValue) -> LoraValue {
match (&lhs, &rhs) {
(LoraValue::Duration(d), LoraValue::Int(n)) if *n != 0 => {
return LoraValue::Duration(d.div_int(*n));
}
_ => {}
}
match (lhs.as_f64(), rhs.as_f64()) {
(Some(_), Some(0.0)) => LoraValue::Null,
(Some(a), Some(b)) => LoraValue::Float(a / b),
_ => LoraValue::Null,
}
}
fn mod_values(lhs: LoraValue, rhs: LoraValue) -> LoraValue {
match (lhs, rhs) {
(LoraValue::Int(_), LoraValue::Int(0)) => LoraValue::Null,
(LoraValue::Int(a), LoraValue::Int(b)) => LoraValue::Int(a % b),
_ => LoraValue::Null,
}
}
fn pow_values(lhs: LoraValue, rhs: LoraValue) -> LoraValue {
match (lhs.as_f64(), rhs.as_f64()) {
(Some(a), Some(b)) => {
let out = a.powf(b);
if out.fract() == 0.0 {
LoraValue::Int(out as i64)
} else {
LoraValue::Float(out)
}
}
_ => LoraValue::Null,
}
}