use std::cmp::Ordering;
use compact_str::CompactString;
use crate::error::{TraverserError, TraverserResult, ValueError, ValueOrigin};
use crate::graph::GraphStorage;
use crate::traversal::{Traverser, TraverserContext};
use crate::value::scalar::compare_scalars;
use crate::value::{ElementProperty, N, TraverserValue};
const VALUE_PREVIEW_LIMIT: usize = 96;
#[derive(Clone, Copy, Debug)]
pub(crate) enum StreamNumber {
Int(i64),
Float(f64),
}
impl StreamNumber {
pub(crate) fn as_f64(self) -> f64 {
match self {
StreamNumber::Int(value) => value as f64,
StreamNumber::Float(value) => value,
}
}
pub(crate) fn times(self, bulk: u64, step: &'static str) -> TraverserResult<StreamNumber> {
if bulk == 1 {
return Ok(self);
}
match self {
StreamNumber::Int(value) => i64::try_from(bulk)
.ok()
.and_then(|bulk| value.checked_mul(bulk))
.map(StreamNumber::Int)
.ok_or_else(|| {
Box::new(TraverserError::Value(ValueError::InvalidNumber {
reason: format!(
"'{step}' overflowed the signed 64-bit integer range at value {value}"
),
}))
}),
StreamNumber::Float(value) => Ok(StreamNumber::Float(value * bulk as f64)),
}
}
}
pub(crate) fn preview(value: &ElementProperty) -> String {
let rendered = crate::value::literal::rhai_literal(value);
if rendered.chars().count() <= VALUE_PREVIEW_LIMIT {
return rendered;
}
let truncated: String = rendered.chars().take(VALUE_PREVIEW_LIMIT).collect();
format!("{truncated}…")
}
pub(crate) enum ValueSource<S: GraphStorage> {
Anonymous,
Vertex(S::VertexId, Option<CompactString>),
Edge(S::EdgeId, Option<CompactString>),
}
impl<S: GraphStorage> ValueSource<S> {
pub(crate) fn origin(&self, context: &TraverserContext<'_, S>) -> Option<ValueOrigin> {
match self {
ValueSource::Anonymous => None,
ValueSource::Vertex(node, property) => Some(ValueOrigin::vertex(
context.storage.vertex_id(*node).map(|id| id.to_string()),
context.storage.vertex_label(*node).map(|l| l.to_string()),
property.as_ref().map(|p| p.to_string()),
)),
ValueSource::Edge(edge, property) => Some(ValueOrigin::edge(
context.storage.edge_id(*edge).map(|id| id.to_string()),
context.storage.edge_label(*edge).map(|l| l.to_string()),
property.as_ref().map(|p| p.to_string()),
)),
}
}
}
pub(crate) fn number_of<S: GraphStorage>(
context: &TraverserContext<'_, S>,
value: &ElementProperty,
source: &ValueSource<S>,
) -> TraverserResult<Option<StreamNumber>> {
match value {
ElementProperty::Null => Ok(None),
ElementProperty::Number(number) => {
let number = match &number.n {
N::NegInt(value) => StreamNumber::Int(*value),
N::Float(value) => StreamNumber::Float(**value),
};
Ok(Some(number))
}
other => Err(Box::new(TraverserError::cast_exception_at(
&["number"],
crate::graph::schema::value_type_name(other),
Some(preview(other)),
source.origin(context),
))),
}
}
pub(crate) fn resolve_number<S: GraphStorage>(
context: &TraverserContext<'_, S>,
traverser: Traverser<S>,
) -> TraverserResult<Option<(StreamNumber, ValueSource<S>)>> {
resolve_value_number(context, traverser.value)
}
pub(crate) fn resolve_value_number<S: GraphStorage>(
context: &TraverserContext<'_, S>,
value: TraverserValue<S>,
) -> TraverserResult<Option<(StreamNumber, ValueSource<S>)>> {
match value {
TraverserValue::Computed(value, node) => {
let source = match node {
Some(node) => ValueSource::Vertex(node, None),
None => ValueSource::Anonymous,
};
Ok(number_of(context, &value, &source)?.map(|number| (number, source)))
}
TraverserValue::VertexProperty(property, node, _) => {
let value = context
.storage
.vertex_properties(node)
.and_then(|properties| properties.get(&property));
match value {
Some(value) => {
let source = ValueSource::Vertex(node, Some(property));
Ok(number_of(context, value, &source)?.map(|number| (number, source)))
}
None => Ok(None),
}
}
TraverserValue::EdgeProperty(property, edge, _) => {
let value = context
.storage
.edge_properties(edge)
.and_then(|properties| properties.get(&property));
match value {
Some(value) => {
let source = ValueSource::Edge(edge, Some(property));
Ok(number_of(context, value, &source)?.map(|number| (number, source)))
}
None => Ok(None),
}
}
other => Err(Box::new(TraverserError::cast_exception_str(
&["computed", "vertexProperty", "edgeProperty"],
other.type_name().to_string(),
))),
}
}
pub(crate) struct MeanAccumulator {
total: f64,
count: u64,
}
impl MeanAccumulator {
pub(crate) fn new() -> Self {
MeanAccumulator {
total: 0.0,
count: 0,
}
}
pub(crate) fn push(&mut self, number: StreamNumber) {
self.total += number.as_f64();
self.count += 1;
}
pub(crate) fn push_n(&mut self, number: StreamNumber, bulk: u64) -> TraverserResult<()> {
self.count = self.count.checked_add(bulk).ok_or_else(|| {
Box::new(TraverserError::BulkOverflow {
step: "mean".to_string(),
})
})?;
self.total += number.as_f64() * bulk as f64;
Ok(())
}
pub(crate) fn finish(self) -> Option<ElementProperty> {
if self.count == 0 {
return None;
}
Some(ElementProperty::Number(
(self.total / self.count as f64).into(),
))
}
}
pub(crate) enum WideningNumeric {
Empty,
Int(i64),
Float(f64),
}
impl WideningNumeric {
pub(crate) fn new() -> Self {
WideningNumeric::Empty
}
pub(crate) fn push(
&mut self,
step: &'static str,
number: StreamNumber,
combine_int: fn(i64, i64) -> Option<i64>,
combine_float: fn(f64, f64) -> f64,
) -> TraverserResult<()> {
*self = match (&*self, number) {
(WideningNumeric::Empty, StreamNumber::Int(value)) => WideningNumeric::Int(value),
(WideningNumeric::Empty, StreamNumber::Float(value)) => WideningNumeric::Float(value),
(WideningNumeric::Int(accumulated), StreamNumber::Int(value)) => {
match combine_int(*accumulated, value) {
Some(combined) => WideningNumeric::Int(combined),
None => {
return Err(Box::new(TraverserError::Value(ValueError::InvalidNumber {
reason: format!(
"'{step}' overflowed the signed 64-bit integer range at value {value}"
),
})));
}
}
}
(WideningNumeric::Float(accumulated), StreamNumber::Float(value)) => {
WideningNumeric::Float(combine_float(*accumulated, value))
}
(WideningNumeric::Int(accumulated), StreamNumber::Float(value)) => {
WideningNumeric::Float(combine_float(*accumulated as f64, value))
}
(WideningNumeric::Float(accumulated), StreamNumber::Int(value)) => {
WideningNumeric::Float(combine_float(*accumulated, value as f64))
}
};
Ok(())
}
pub(crate) fn finish(self) -> Option<ElementProperty> {
match self {
WideningNumeric::Empty => None,
WideningNumeric::Int(value) => Some(ElementProperty::Number(value.into())),
WideningNumeric::Float(value) => Some(ElementProperty::Number(value.into())),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Extremum {
Min,
Max,
}
impl Extremum {
fn name(self) -> &'static str {
match self {
Extremum::Min => "min",
Extremum::Max => "max",
}
}
fn pick(self) -> Ordering {
match self {
Extremum::Min => Ordering::Less,
Extremum::Max => Ordering::Greater,
}
}
}
const COMPARABLE_KINDS: &[&str] = &["number", "string", "boolean", "uuid"];
pub(crate) struct ExtremumAccumulator {
which: Extremum,
numbers: WideningNumeric,
other: Option<ElementProperty>,
}
impl ExtremumAccumulator {
pub(crate) fn new(which: Extremum) -> Self {
ExtremumAccumulator {
which,
numbers: WideningNumeric::new(),
other: None,
}
}
pub(crate) fn push_property<S: GraphStorage>(
&mut self,
value: &ElementProperty,
context: &TraverserContext<'_, S>,
source: &ValueSource<S>,
) -> TraverserResult<()> {
let step = self.which.name();
let held_kind = if self.other.is_some() {
self.other.as_ref().map(kind_name)
} else if matches!(self.numbers, WideningNumeric::Empty) {
None
} else {
Some("number")
};
match value {
ElementProperty::Null => Ok(()),
ElementProperty::Number(_) => {
if let Some(held) = held_kind.filter(|held| *held != "number") {
return Err(incomparable(step, &[held], value, context, source));
}
let Some(number) = number_of(context, value, source)? else {
return Ok(());
};
match self.which {
Extremum::Min => {
self.numbers
.push(step, number, |a, b| Some(a.min(b)), f64::min)
}
Extremum::Max => {
self.numbers
.push(step, number, |a, b| Some(a.max(b)), f64::max)
}
}
}
ElementProperty::String(_)
| ElementProperty::Compressed(_)
| ElementProperty::Boolean(_)
| ElementProperty::Uuid(_) => {
match (&self.other, held_kind) {
(None, None) => self.other = Some(value.to_logical()),
(Some(held), _) if kind_name(held) == kind_name(value) => {
if compare_scalars(value, held) == Some(self.which.pick()) {
self.other = Some(value.to_logical());
}
}
(_, held) => {
let expected = held.unwrap_or("number");
return Err(incomparable(step, &[expected], value, context, source));
}
}
Ok(())
}
ElementProperty::Object(_)
| ElementProperty::Array(_)
| ElementProperty::Vertex(_)
| ElementProperty::Edge(_)
| ElementProperty::Map(_) => {
Err(incomparable(step, COMPARABLE_KINDS, value, context, source))
}
}
}
pub(crate) fn finish(self) -> Option<ElementProperty> {
match self.other {
Some(value) => Some(value),
None => self.numbers.finish(),
}
}
}
fn kind_name(value: &ElementProperty) -> &'static str {
match value {
ElementProperty::String(_) | ElementProperty::Compressed(_) => "string",
ElementProperty::Boolean(_) => "boolean",
ElementProperty::Uuid(_) => "uuid",
ElementProperty::Number(_) => "number",
ElementProperty::Null
| ElementProperty::Object(_)
| ElementProperty::Array(_)
| ElementProperty::Vertex(_)
| ElementProperty::Edge(_)
| ElementProperty::Map(_) => "object",
}
}
fn incomparable<S: GraphStorage>(
step: &'static str,
expected: &[&'static str],
offending: &ElementProperty,
context: &TraverserContext<'_, S>,
source: &ValueSource<S>,
) -> crate::error::BoxedTraverserError {
Box::new(TraverserError::Value(ValueError::IncomparableValues {
step,
expected: expected.join(" or "),
actual: crate::graph::schema::value_type_name(offending),
value: Some(preview(offending)),
origin: source.origin(context).map(Box::new),
}))
}
pub(crate) fn push_extremum_value<S: GraphStorage>(
accumulator: &mut ExtremumAccumulator,
context: &TraverserContext<'_, S>,
value: TraverserValue<S>,
) -> TraverserResult<()> {
match value {
TraverserValue::Computed(value, node) => {
let source = match node {
Some(node) => ValueSource::Vertex(node, None),
None => ValueSource::Anonymous,
};
accumulator.push_property(&value, context, &source)
}
TraverserValue::VertexProperty(property, node, _) => {
let value = context
.storage
.vertex_properties(node)
.and_then(|properties| properties.get(&property));
match value {
Some(value) => {
let source = ValueSource::Vertex(node, Some(property));
accumulator.push_property(value, context, &source)
}
None => Ok(()),
}
}
TraverserValue::EdgeProperty(property, edge, _) => {
let value = context
.storage
.edge_properties(edge)
.and_then(|properties| properties.get(&property));
match value {
Some(value) => {
let source = ValueSource::Edge(edge, Some(property));
accumulator.push_property(value, context, &source)
}
None => Ok(()),
}
}
other => Err(Box::new(TraverserError::Value(
ValueError::IncomparableValues {
step: accumulator.which.name(),
expected: COMPARABLE_KINDS.join(" or "),
actual: other.type_name().to_string(),
value: None,
origin: None,
},
))),
}
}