use super::predicate::{predicate_comparison_truth, PredicateTruth};
use super::*;
use hawdb_plan_cypher::ScalarBinaryOp;
mod borrowed;
pub(crate) use borrowed::{evaluate_projection_borrowed, ProjectedValue};
pub fn insert_projected_value<V>(values: &mut BTreeMap<String, V>, name: &str, value: V) {
let mut candidate = name.to_string();
let mut suffix = 2;
loop {
match values.entry(candidate) {
std::collections::btree_map::Entry::Vacant(entry) => {
entry.insert(value);
return;
}
std::collections::btree_map::Entry::Occupied(_) => {
candidate = format!("{name}#{suffix}");
suffix += 1;
}
}
}
}
pub fn project_value(item: &Projection, catalog: &Catalog, binding: &Binding) -> Result<Value> {
evaluate_projection_expression(&item.expression, catalog, binding)
}
pub fn evaluate_projection_expression(
expression: &ProjectionExpression,
catalog: &Catalog,
binding: &Binding,
) -> Result<Value> {
evaluate_projection_borrowed(expression, catalog, binding, None).map(ProjectedValue::into_owned)
}
pub(super) fn project_expression_value(
expression: &ProjectionExpression,
catalog: &Catalog,
binding: &Binding,
) -> Result<Value> {
evaluate_projection_expression(expression, catalog, binding)
}
fn coalesce_difference(
binding: &Binding,
variable: &str,
terms: &[CoalesceDifferenceProjectionTerm],
) -> Result<i64> {
let Some((first, rest)) = terms.split_first() else {
return Err(HawDBError::Execution(
"coalesce difference requires at least one term".to_string(),
));
};
let mut value = coalesce_integer_term(binding, variable, first)?;
for term in rest {
value -= coalesce_integer_term(binding, variable, term)?;
}
Ok(value)
}
fn coalesce_integer_term(
binding: &Binding,
variable: &str,
term: &CoalesceDifferenceProjectionTerm,
) -> Result<i64> {
match binding_property(binding, variable, &term.property) {
Some(Value::Int(value)) => Ok(*value),
Some(Value::Null) | None => integer_value(&term.default, "COALESCE default"),
Some(value) => Err(HawDBError::Execution(format!(
"COALESCE difference requires integer property '{}.{}', got {value:?}",
variable, term.property
))),
}
}
fn integer_value(value: &Value, context: &str) -> Result<i64> {
match value {
Value::Int(value) => Ok(*value),
value => Err(HawDBError::Execution(format!(
"{context} requires an integer value, got {value:?}"
))),
}
}
pub fn group_key_value(item: &Projection, catalog: &Catalog, binding: &Binding) -> Value {
project_value(item, catalog, binding).unwrap_or(Value::Null)
}
fn timestamp_date_part(part: DatePart, nanos: i64) -> i64 {
let days = div_floor(nanos, 86_400_000_000_000);
let (year, month, _) = civil_from_days(days);
match part {
DatePart::Year => year as i64,
DatePart::Month => month as i64,
}
}
fn div_floor(value: i64, divisor: i64) -> i64 {
let quotient = value / divisor;
let remainder = value % divisor;
if remainder != 0 && ((remainder < 0) != (divisor < 0)) {
quotient - 1
} else {
quotient
}
}
fn civil_from_days(days: i64) -> (i32, u32, u32) {
let days = days + 719_468;
let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
let day_of_era = days - era * 146_097;
let year_of_era =
(day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
let year = year_of_era + era * 400;
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
let month_prime = (5 * day_of_year + 2) / 153;
let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
let month = month_prime + if month_prime < 10 { 3 } else { -9 };
let year = year + i64::from(month <= 2);
(year as i32, month as u32, day as u32)
}
fn binding_declares_variable(binding: &Binding, variable: &str) -> bool {
binding_has_variable(binding, variable) || binding.values.get(variable) == Some(&Value::Null)
}
pub fn binding_has_variable(binding: &Binding, variable: &str) -> bool {
binding.nodes.contains_key(variable) || binding.relationships.contains_key(variable)
}
pub fn binding_property<'a>(
binding: &'a Binding,
variable: &str,
property: &str,
) -> Option<&'a Value> {
binding
.nodes
.get(variable)
.and_then(|node| node.properties.get(property))
.or_else(|| {
binding
.relationships
.get(variable)
.and_then(|relationship| relationship.properties.get(property))
})
}
pub fn binding_value(binding: &Binding, catalog: &Catalog, variable: &str) -> Option<Value> {
binding
.nodes
.get(variable)
.map(|node| node_value(node, catalog))
.or_else(|| {
binding
.relationships
.get(variable)
.map(|relationship| relationship_value(relationship, catalog))
})
.or_else(|| (binding.values.get(variable) == Some(&Value::Null)).then_some(Value::Null))
}
fn node_value(node: &NodeRecord, catalog: &Catalog) -> Value {
let mut values = node.properties.clone();
values.insert("_id".to_string(), Value::Int(node.id.0 as i64));
values.insert(
"labels".to_string(),
Value::List(
node.labels
.iter()
.filter_map(|label_id| catalog.label_name(*label_id))
.map(|label| Value::String(label.to_string()))
.collect(),
),
);
Value::Map(values)
}
fn relationship_value(relationship: &RelRecord, catalog: &Catalog) -> Value {
let mut values = relationship.properties.clone();
values.insert("_id".to_string(), Value::Int(relationship.id.0 as i64));
values.insert(
"source_id".to_string(),
Value::Int(relationship.source.0 as i64),
);
values.insert(
"target_id".to_string(),
Value::Int(relationship.target.0 as i64),
);
values.insert(
"type".to_string(),
catalog
.rel_type_name(relationship.rel_type)
.map(|rel_type| Value::String(rel_type.to_string()))
.unwrap_or(Value::Null),
);
Value::Map(values)
}
pub(super) fn binding_id(binding: &Binding, variable: &str) -> Option<Value> {
binding
.nodes
.get(variable)
.map(|node| Value::Int(node.id.0 as i64))
.or_else(|| {
binding
.relationships
.get(variable)
.map(|relationship| Value::Int(relationship.id.0 as i64))
})
}
pub fn binding_identity_key(binding: &Binding, variable: &str) -> Option<(u8, u64)> {
binding
.nodes
.get(variable)
.map(|node| (0, node.id.0))
.or_else(|| {
binding
.relationships
.get(variable)
.map(|relationship| (1, relationship.id.0))
})
}
fn scalar_truth(value: &Value) -> Result<PredicateTruth> {
match value {
Value::Bool(value) => Ok(PredicateTruth::from_bool(*value)),
Value::Null => Ok(PredicateTruth::Unknown),
value => Err(HawDBError::Execution(format!(
"CASE condition requires a boolean value, got {value:?}"
))),
}
}
fn truth_value(truth: PredicateTruth) -> Value {
match truth {
PredicateTruth::True => Value::Bool(true),
PredicateTruth::False => Value::Bool(false),
PredicateTruth::Unknown => Value::Null,
}
}
fn evaluate_arithmetic(left: &Value, op: ScalarBinaryOp, right: &Value) -> Result<Value> {
if matches!(left, Value::Null) || matches!(right, Value::Null) {
return Ok(Value::Null);
}
if let (Value::Int(left), Value::Int(right)) = (left, right) {
if matches!(op, ScalarBinaryOp::Divide | ScalarBinaryOp::Remainder) && *right == 0 {
return Err(HawDBError::Execution("division by zero".to_string()));
}
let value = match op {
ScalarBinaryOp::Add => left.checked_add(*right),
ScalarBinaryOp::Subtract => left.checked_sub(*right),
ScalarBinaryOp::Multiply => left.checked_mul(*right),
ScalarBinaryOp::Divide => left.checked_div(*right),
ScalarBinaryOp::Remainder => left.checked_rem(*right),
_ => unreachable!("arithmetic operators only"),
};
return value
.map(Value::Int)
.ok_or_else(|| HawDBError::Execution("integer arithmetic overflow".to_string()));
}
let numeric = |value: &Value| match value {
Value::Int(value) => Ok(*value as f64),
Value::Float(value) => Ok(*value),
_ => Err(HawDBError::Execution(
"arithmetic requires numeric operands".to_string(),
)),
};
let left = numeric(left)?;
let right = numeric(right)?;
if matches!(op, ScalarBinaryOp::Divide | ScalarBinaryOp::Remainder) && right == 0.0 {
return Err(HawDBError::Execution("division by zero".to_string()));
}
let value = match op {
ScalarBinaryOp::Add => left + right,
ScalarBinaryOp::Subtract => left - right,
ScalarBinaryOp::Multiply => left * right,
ScalarBinaryOp::Divide => left / right,
ScalarBinaryOp::Remainder => left % right,
_ => unreachable!("arithmetic operators only"),
};
if !value.is_finite() {
return Err(HawDBError::Execution(
"non-finite arithmetic result".to_string(),
));
}
Ok(Value::Float(value))
}
#[cfg(test)]
mod case_tests {
use super::*;
fn literal(value: Value) -> ProjectionExpression {
ProjectionExpression::Literal(value)
}
fn evaluate(expression: &ProjectionExpression) -> Result<Value> {
evaluate_projection_expression(
expression,
&Catalog::default(),
&Binding::values(BTreeMap::new()),
)
}
fn invalid() -> ProjectionExpression {
ProjectionExpression::Lower(Box::new(literal(Value::Int(7))))
}
#[test]
fn derived_column_rank_matches_generic_case_across_types_and_missing_values() {
let values = [
Value::Null,
Value::String("first".into()),
Value::String("first second".into()),
Value::Int(1),
Value::Bool(true),
Value::Float(1.5),
Value::List(vec![Value::Int(1)]),
];
for raw in &values {
for normalized in &values {
let binary = |op, value: &Value| ProjectionExpression::Binary {
left: Box::new(ProjectionExpression::Column("value".into())),
op,
right: Box::new(literal(value.clone())),
};
let generic = ProjectionExpression::Case {
operand: None,
branches: vec![
(binary(ScalarBinaryOp::Eq, raw), literal(Value::Int(3))),
(
binary(ScalarBinaryOp::Eq, normalized),
literal(Value::Int(3)),
),
(
binary(ScalarBinaryOp::Contains, raw),
literal(Value::Int(2)),
),
(
binary(ScalarBinaryOp::Contains, normalized),
literal(Value::Int(2)),
),
],
otherwise: Some(Box::new(literal(Value::Int(1)))),
};
let specialized = ProjectionExpression::CaseColumnSearchRank(Box::new(
hawdb_plan_cypher::CaseColumnSearchRankProjection {
column: "value".into(),
raw_query: raw.clone(),
normalized_query: normalized.clone(),
exact_rank: Value::Int(3),
contains_rank: Value::Int(2),
fallback_rank: Value::Int(1),
},
));
for column in values.iter().map(Some).chain([None]) {
let binding = Binding::values(
column
.map(|value| BTreeMap::from([("value".into(), value.clone())]))
.unwrap_or_default(),
);
let evaluate = |expression| {
evaluate_projection_expression(expression, &Catalog::default(), &binding)
.map_err(|error| error.to_string())
};
assert_eq!(
evaluate(&specialized),
evaluate(&generic),
"raw={raw:?} normalized={normalized:?} column={column:?}"
);
}
}
}
}
#[test]
fn derived_entity_rank_matches_generic_case_for_values_nulls_and_errors() {
let property = |property: &str| ProjectionExpression::Property {
variable: "n".into(),
property: property.into(),
};
let comparison = |query: Value| ProjectionExpression::Binary {
left: Box::new(ProjectionExpression::Lower(Box::new(property("name")))),
op: ScalarBinaryOp::Eq,
right: Box::new(literal(query)),
};
let raw = Value::String("first".into());
let normalized = Value::String("second".into());
let alias = Value::String("alias".into());
let generic = ProjectionExpression::Case {
operand: None,
branches: vec![
(comparison(raw.clone()), literal(Value::Int(0))),
(comparison(normalized.clone()), literal(Value::Int(0))),
(
ProjectionExpression::Binary {
left: Box::new(property("aliases")),
op: ScalarBinaryOp::ListContains,
right: Box::new(literal(alias.clone())),
},
literal(Value::Int(1)),
),
],
otherwise: Some(Box::new(literal(Value::Int(2)))),
};
let specialized = ProjectionExpression::CaseEntitySearchRank(Box::new(
hawdb_plan_cypher::CaseEntitySearchRankProjection {
variable: "n".into(),
name_property: "name".into(),
aliases_property: "aliases".into(),
raw_query: raw,
normalized_query: normalized,
raw_input: alias.clone(),
exact_rank: Value::Int(0),
alias_rank: Value::Int(1),
fallback_rank: Value::Int(2),
},
));
for name in [
None,
Some(Value::Null),
Some(Value::String("FIRST".into())),
Some(Value::String("SECOND".into())),
Some(Value::String("other".into())),
Some(Value::Int(5)),
Some(Value::Bool(false)),
] {
for aliases in [
Value::Null,
Value::List(vec![alias.clone()]),
Value::List(vec![]),
Value::Int(7),
] {
let mut properties = BTreeMap::from([("aliases".into(), aliases)]);
if let Some(name) = &name {
properties.insert("name".into(), name.clone());
}
let mut binding = Binding::values(BTreeMap::new());
binding.nodes.insert(
"n".into(),
NodeRecord {
id: hawdb_storage::NodeId(1),
labels: Default::default(),
properties,
},
);
let evaluate = |expression| {
evaluate_projection_expression(expression, &Catalog::default(), &binding)
.map_err(|error| error.to_string())
};
assert_eq!(evaluate(&specialized), evaluate(&generic));
}
}
}
#[test]
fn case_is_lazy_and_null_does_not_select_a_branch() {
let expression = ProjectionExpression::Case {
operand: None,
branches: vec![
(literal(Value::Null), invalid()),
(literal(Value::Bool(true)), literal(Value::Int(9))),
(invalid(), invalid()),
],
otherwise: Some(Box::new(invalid())),
};
assert_eq!(evaluate(&expression).unwrap(), Value::Int(9));
let expression = ProjectionExpression::Case {
operand: None,
branches: vec![(literal(Value::Bool(false)), invalid())],
otherwise: None,
};
assert_eq!(evaluate(&expression).unwrap(), Value::Null);
}
#[test]
fn simple_case_null_does_not_equal_null_and_selected_errors_propagate() {
let expression = ProjectionExpression::Case {
operand: Some(Box::new(literal(Value::Null))),
branches: vec![(literal(Value::Null), invalid())],
otherwise: Some(Box::new(literal(Value::Int(4)))),
};
assert_eq!(evaluate(&expression).unwrap(), Value::Int(4));
for condition in [literal(Value::Int(1)), literal(Value::Bool(true))] {
let expression = ProjectionExpression::Case {
operand: None,
branches: vec![(condition, invalid())],
otherwise: None,
};
assert!(evaluate(&expression).is_err());
}
}
#[test]
fn conditional_boolean_operators_preserve_three_valued_truth_and_short_circuit() {
let values = [Value::Bool(false), Value::Bool(true), Value::Null];
let and = [
[Some(false), Some(false), Some(false)],
[Some(false), Some(true), None],
[Some(false), None, None],
];
let or = [
[Some(false), Some(true), None],
[Some(true), Some(true), Some(true)],
[None, Some(true), None],
];
for (op, truth) in [(ScalarBinaryOp::And, and), (ScalarBinaryOp::Or, or)] {
for (i, left) in values.iter().enumerate() {
for (j, right) in values.iter().enumerate() {
let expression = ProjectionExpression::Binary {
left: Box::new(literal(left.clone())),
op,
right: Box::new(literal(right.clone())),
};
assert_eq!(
evaluate(&expression).unwrap(),
truth[i][j].map(Value::Bool).unwrap_or(Value::Null)
);
}
}
}
for (op, left) in [(ScalarBinaryOp::And, false), (ScalarBinaryOp::Or, true)] {
let expression = ProjectionExpression::Binary {
left: Box::new(literal(Value::Bool(left))),
op,
right: Box::new(invalid()),
};
assert_eq!(evaluate(&expression).unwrap(), Value::Bool(left));
}
assert_eq!(
evaluate(&ProjectionExpression::Not(Box::new(literal(Value::Null)))).unwrap(),
Value::Null
);
}
}
#[cfg(test)]
mod arithmetic_tests {
use super::*;
#[test]
fn arithmetic_preserves_numeric_types_and_nulls() {
for (op, left, right, expected) in [
(
ScalarBinaryOp::Add,
Value::Int(2),
Value::Int(3),
Value::Int(5),
),
(
ScalarBinaryOp::Subtract,
Value::Int(2),
Value::Int(3),
Value::Int(-1),
),
(
ScalarBinaryOp::Multiply,
Value::Int(2),
Value::Int(3),
Value::Int(6),
),
(
ScalarBinaryOp::Divide,
Value::Int(7),
Value::Int(3),
Value::Int(2),
),
(
ScalarBinaryOp::Remainder,
Value::Int(-7),
Value::Int(3),
Value::Int(-1),
),
(
ScalarBinaryOp::Add,
Value::Float(1.5),
Value::Int(2),
Value::Float(3.5),
),
(
ScalarBinaryOp::Divide,
Value::Int(7),
Value::Float(2.0),
Value::Float(3.5),
),
(
ScalarBinaryOp::Multiply,
Value::Int(7),
Value::Null,
Value::Null,
),
(ScalarBinaryOp::Add, Value::Null, Value::Int(7), Value::Null),
] {
assert_eq!(evaluate_arithmetic(&left, op, &right).unwrap(), expected);
}
}
#[test]
fn arithmetic_errors_propagate_through_projection() {
let catalog = Catalog::default();
let binding = Binding::values(BTreeMap::new());
for (op, left, right, message) in [
(
ScalarBinaryOp::Add,
Value::Int(i64::MAX),
Value::Int(1),
"overflow",
),
(
ScalarBinaryOp::Divide,
Value::Int(i64::MIN),
Value::Int(-1),
"overflow",
),
(
ScalarBinaryOp::Remainder,
Value::Int(i64::MIN),
Value::Int(-1),
"overflow",
),
(
ScalarBinaryOp::Divide,
Value::Int(1),
Value::Int(0),
"division by zero",
),
(
ScalarBinaryOp::Remainder,
Value::Float(1.0),
Value::Float(-0.0),
"division by zero",
),
(
ScalarBinaryOp::Add,
Value::String("1".to_string()),
Value::Int(1),
"numeric operands",
),
(
ScalarBinaryOp::Multiply,
Value::Float(f64::MAX),
Value::Float(2.0),
"non-finite",
),
] {
let expression = ProjectionExpression::Binary {
left: Box::new(ProjectionExpression::Literal(left)),
op,
right: Box::new(ProjectionExpression::Literal(right)),
};
let error =
evaluate_projection_expression(&expression, &catalog, &binding).unwrap_err();
assert!(error.to_string().contains(message), "{error}");
}
}
}