#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_sign_loss)]
use crate::collection::types::Collection;
use crate::velesql::{
AggregateArg, AggregateFunction, AggregateResult, AggregateType, CompareOp, HavingClause, Value,
};
use std::collections::HashMap;
pub(super) const DEFAULT_MAX_GROUPS: usize = 10_000;
pub(super) const SERVER_MAX_GROUPS_CEILING: usize = 1_000_000;
impl Collection {
pub(crate) fn sort_aggregation_results(
results: &mut [serde_json::Value],
order_by: &[crate::velesql::SelectOrderBy],
) {
use crate::velesql::OrderByExpr;
let sort_columns: Vec<(String, bool)> = order_by
.iter()
.filter_map(|clause| {
let column = match &clause.expr {
OrderByExpr::Field(name) => name.clone(),
OrderByExpr::Aggregate(agg) => Self::aggregation_result_key(agg),
OrderByExpr::Similarity(_)
| OrderByExpr::SimilarityBare
| OrderByExpr::Arithmetic(_) => return None,
};
Some((column, clause.descending))
})
.collect();
results.sort_unstable_by(|a, b| {
for (column, descending) in &sort_columns {
let val_a = a.get(column);
let val_b = b.get(column);
let ordering =
crate::collection::search::query::ordering::compare_json_values(val_a, val_b);
let ordering = if *descending {
ordering.reverse()
} else {
ordering
};
if ordering != std::cmp::Ordering::Equal {
return ordering;
}
}
std::cmp::Ordering::Equal
});
}
pub(super) fn extract_group_key_fast(
payload: Option<&serde_json::Value>,
group_by_columns: &[String],
) -> super::GroupKey {
let values: Vec<serde_json::Value> = group_by_columns
.iter()
.map(|col| {
payload
.and_then(|p| Self::get_nested_value(p, col).cloned())
.unwrap_or(serde_json::Value::Null)
})
.collect();
super::GroupKey::new(values)
}
pub(super) fn evaluate_having(having: &HavingClause, agg_result: &AggregateResult) -> bool {
if having.conditions.is_empty() {
return true;
}
let mut result = {
let cond = &having.conditions[0];
let agg_value = Self::get_aggregate_value(&cond.aggregate, agg_result);
Self::compare_values(agg_value, cond.operator, &cond.value)
};
for (i, cond) in having.conditions.iter().enumerate().skip(1) {
let cond_result = {
let agg_value = Self::get_aggregate_value(&cond.aggregate, agg_result);
Self::compare_values(agg_value, cond.operator, &cond.value)
};
let op = having
.operators
.get(i - 1)
.copied()
.unwrap_or(crate::velesql::LogicalOp::And);
match op {
crate::velesql::LogicalOp::And => result = result && cond_result,
crate::velesql::LogicalOp::Or => result = result || cond_result,
}
}
result
}
fn get_aggregate_value(agg: &AggregateFunction, result: &AggregateResult) -> Option<f64> {
match (&agg.function_type, &agg.argument) {
(AggregateType::Count, AggregateArg::Wildcard) => Some(result.count as f64),
(AggregateType::Count, AggregateArg::Column(col)) => {
result.counts.get(col.as_str()).map(|&c| c as f64)
}
(AggregateType::Sum, AggregateArg::Column(col)) => {
result.sums.get(col.as_str()).copied()
}
(AggregateType::Avg, AggregateArg::Column(col)) => {
result.avgs.get(col.as_str()).copied()
}
(AggregateType::Min, AggregateArg::Column(col)) => {
result.mins.get(col.as_str()).copied()
}
(AggregateType::Max, AggregateArg::Column(col)) => {
result.maxs.get(col.as_str()).copied()
}
_ => None,
}
}
fn compare_values(agg_value: Option<f64>, op: CompareOp, threshold: &Value) -> bool {
let Some(agg) = agg_value else {
return false;
};
let thresh = match threshold {
Value::Integer(i) => *i as f64,
#[allow(clippy::cast_precision_loss)]
Value::UnsignedInteger(u) => *u as f64,
Value::Float(f) => *f,
_ => return false,
};
let relative_epsilon = f64::EPSILON * agg.abs().max(thresh.abs()).max(1.0);
match op {
CompareOp::Eq => (agg - thresh).abs() < relative_epsilon,
CompareOp::NotEq => (agg - thresh).abs() >= relative_epsilon,
CompareOp::Gt => agg > thresh,
CompareOp::Gte => agg >= thresh,
CompareOp::Lt => agg < thresh,
CompareOp::Lte => agg <= thresh,
}
}
pub(super) fn extract_max_groups_limit(
with_clause: Option<&crate::velesql::WithClause>,
) -> usize {
let Some(with) = with_clause else {
return DEFAULT_MAX_GROUPS;
};
for opt in &with.options {
if opt.key == "max_groups" || opt.key == "group_limit" {
if let crate::velesql::WithValue::Integer(n) = &opt.value {
let limit = (*n).max(1) as usize;
return limit.min(SERVER_MAX_GROUPS_CEILING);
}
}
}
DEFAULT_MAX_GROUPS
}
pub(super) fn resolve_having_params(
having: &HavingClause,
params: &HashMap<String, serde_json::Value>,
) -> crate::error::Result<HavingClause> {
let conditions = having
.conditions
.iter()
.map(|cond| {
Ok(crate::velesql::HavingCondition {
aggregate: cond.aggregate.clone(),
operator: cond.operator,
value: Self::resolve_where_param(&cond.value, params)?,
})
})
.collect::<crate::error::Result<Vec<_>>>()?;
Ok(HavingClause {
conditions,
operators: having.operators.clone(),
})
}
pub(crate) fn resolve_condition_params(
cond: &crate::velesql::Condition,
params: &HashMap<String, serde_json::Value>,
) -> crate::error::Result<crate::velesql::Condition> {
use crate::velesql::Condition;
match cond {
Condition::And(left, right) => Ok(Condition::And(
Self::resolve_boxed_condition(left, params)?,
Self::resolve_boxed_condition(right, params)?,
)),
Condition::Or(left, right) => Ok(Condition::Or(
Self::resolve_boxed_condition(left, params)?,
Self::resolve_boxed_condition(right, params)?,
)),
Condition::Not(inner) => Ok(Condition::Not(Self::resolve_boxed_condition(
inner, params,
)?)),
Condition::Group(inner) => Ok(Condition::Group(Self::resolve_boxed_condition(
inner, params,
)?)),
other => Self::resolve_leaf_condition_params(other, params),
}
}
fn resolve_boxed_condition(
cond: &crate::velesql::Condition,
params: &HashMap<String, serde_json::Value>,
) -> crate::error::Result<Box<crate::velesql::Condition>> {
Ok(Box::new(Self::resolve_condition_params(cond, params)?))
}
fn resolve_leaf_condition_params(
cond: &crate::velesql::Condition,
params: &HashMap<String, serde_json::Value>,
) -> crate::error::Result<crate::velesql::Condition> {
use crate::velesql::Condition;
Ok(match cond {
Condition::Comparison(cmp) => Condition::Comparison(crate::velesql::Comparison {
column: cmp.column.clone(),
operator: cmp.operator,
value: Self::resolve_where_param(&cmp.value, params)?,
}),
Condition::In(in_cond) => Condition::In(crate::velesql::InCondition {
column: in_cond.column.clone(),
values: Self::resolve_value_list(&in_cond.values, params)?,
negated: in_cond.negated,
}),
Condition::Between(btw) => Condition::Between(crate::velesql::BetweenCondition {
column: btw.column.clone(),
low: Self::resolve_where_param(&btw.low, params)?,
high: Self::resolve_where_param(&btw.high, params)?,
}),
Condition::Contains(contains) => {
Condition::Contains(crate::velesql::ContainsCondition {
column: contains.column.clone(),
mode: contains.mode,
values: Self::resolve_value_list(&contains.values, params)?,
})
}
other => other.clone(),
})
}
fn resolve_value_list(
values: &[Value],
params: &HashMap<String, serde_json::Value>,
) -> crate::error::Result<Vec<Value>> {
values
.iter()
.map(|v| Self::resolve_where_param(v, params))
.collect()
}
}