use vibesql_ast::Expression;
use vibesql_storage::Row;
use vibesql_types::SqlValue;
use super::{
evaluator::{eval_simple_expr, sql_value_to_f64},
simd::try_simd_aggregate,
};
use crate::{
errors::ExecutorError,
schema::CombinedSchema,
select::{
columnar::{
aggregate::{functions::compare_for_min_max, AggregateOp},
scan::ColumnarScan,
},
vectorized::DEFAULT_BATCH_SIZE,
},
};
const SIMD_THRESHOLD: usize = 100;
fn try_vectorized_binary_aggregate(
rows: &[Row],
expr: &Expression,
op: AggregateOp,
filter_bitmap: Option<&[bool]>,
schema: &CombinedSchema,
) -> Result<Option<SqlValue>, ExecutorError> {
log::trace!(
"[VecBinary] Checking vectorized binary aggregate for {} rows, op={:?}",
rows.len(),
op
);
if !matches!(op, AggregateOp::Sum | AggregateOp::Avg) {
log::trace!("[VecBinary] Op {:?} not supported for vectorized binary", op);
return Ok(None);
}
if let Expression::BinaryOp { left, op: bin_op, right } = expr {
use vibesql_ast::BinaryOperator;
if *bin_op != BinaryOperator::Multiply {
return Ok(None);
}
let (left_col, right_col) = match (left.as_ref(), right.as_ref()) {
(Expression::ColumnRef(col_id1), Expression::ColumnRef(col_id2))
if col_id1.schema_canonical().is_none() && col_id2.schema_canonical().is_none() =>
{
let c1 = col_id1.column_canonical();
let c2 = col_id2.column_canonical();
let idx1 =
schema.get_column_index(col_id1.table_canonical(), c1).ok_or_else(|| {
ExecutorError::UnsupportedExpression(format!("Column not found: {}", c1))
})?;
let idx2 =
schema.get_column_index(col_id2.table_canonical(), c2).ok_or_else(|| {
ExecutorError::UnsupportedExpression(format!("Column not found: {}", c2))
})?;
(idx1, idx2)
}
_ => return Ok(None), };
let scan = ColumnarScan::new(rows);
let mut sum = 0.0;
let mut count = 0;
let mut batch_products = Vec::with_capacity(DEFAULT_BATCH_SIZE);
for row_idx in 0..rows.len() {
if let Some(bitmap) = filter_bitmap {
if !bitmap.get(row_idx).copied().unwrap_or(false) {
continue;
}
}
let val1 =
scan.row(row_idx).and_then(|row| row.get(left_col)).unwrap_or(&SqlValue::Null);
let val2 =
scan.row(row_idx).and_then(|row| row.get(right_col)).unwrap_or(&SqlValue::Null);
if let (Some(v1), Some(v2)) = (sql_value_to_f64(val1), sql_value_to_f64(val2)) {
batch_products.push(v1 * v2);
count += 1;
if batch_products.len() >= DEFAULT_BATCH_SIZE {
sum += batch_products.iter().sum::<f64>();
batch_products.clear();
}
}
}
if !batch_products.is_empty() {
sum += batch_products.iter().sum::<f64>();
}
let result = if count > 0 {
match op {
AggregateOp::Sum => SqlValue::Double(sum),
AggregateOp::Avg => SqlValue::Double(sum / count as f64),
_ => unreachable!(),
}
} else {
SqlValue::Null
};
return Ok(Some(result));
}
Ok(None)
}
pub fn compute_expression_aggregate(
rows: &[Row],
expr: &Expression,
op: AggregateOp,
filter_bitmap: Option<&[bool]>,
schema: &CombinedSchema,
) -> Result<SqlValue, ExecutorError> {
log::debug!(
"[ExprAgg] compute_expression_aggregate: {} rows, op={:?}, has_filter={}, threshold={}",
rows.len(),
op,
filter_bitmap.is_some(),
SIMD_THRESHOLD
);
if let Some(result) = try_vectorized_binary_aggregate(rows, expr, op, filter_bitmap, schema)? {
log::debug!("[ExprAgg] Used vectorized binary aggregate path");
return Ok(result);
}
if rows.len() >= SIMD_THRESHOLD && filter_bitmap.is_none() {
log::debug!(
"[ExprAgg] Attempting Arrow SIMD path ({} >= {} rows)",
rows.len(),
SIMD_THRESHOLD
);
if let Ok(result) = try_simd_aggregate(rows, expr, op, schema) {
log::debug!("[ExprAgg] Arrow SIMD path succeeded");
return Ok(result);
}
log::debug!("[ExprAgg] Arrow SIMD path failed, falling back to scalar");
} else if rows.len() < SIMD_THRESHOLD {
log::debug!(
"[ExprAgg] Below SIMD threshold ({} < {}), using vectorized/scalar path",
rows.len(),
SIMD_THRESHOLD
);
}
log::debug!("[ExprAgg] Using scalar aggregate path");
compute_scalar_aggregate(rows, expr, op, filter_bitmap, schema)
}
fn compute_scalar_aggregate(
rows: &[Row],
expr: &Expression,
op: AggregateOp,
filter_bitmap: Option<&[bool]>,
schema: &CombinedSchema,
) -> Result<SqlValue, ExecutorError> {
match op {
AggregateOp::Sum => {
let mut int_sum: i64 = 0;
let mut float_sum = 0.0;
let mut count = 0;
let mut has_float = false;
for (row_idx, row) in rows.iter().enumerate() {
if let Some(bitmap) = filter_bitmap {
if !bitmap.get(row_idx).copied().unwrap_or(false) {
continue;
}
}
let value = eval_simple_expr(expr, row, schema)?;
if !matches!(value, SqlValue::Null) {
match value {
SqlValue::Integer(v) => {
if has_float {
float_sum += v as f64;
} else {
int_sum += v;
}
}
SqlValue::Bigint(v) => {
if has_float {
float_sum += v as f64;
} else {
int_sum += v;
}
}
SqlValue::Smallint(v) => {
if has_float {
float_sum += v as f64;
} else {
int_sum += v as i64;
}
}
SqlValue::Float(v) => {
if !has_float {
float_sum = int_sum as f64;
has_float = true;
}
float_sum += v as f64;
}
SqlValue::Double(v) => {
if !has_float {
float_sum = int_sum as f64;
has_float = true;
}
float_sum += v;
}
SqlValue::Numeric(v) => {
if !has_float {
float_sum = int_sum as f64;
has_float = true;
}
float_sum += v;
}
SqlValue::Null => {} _ => {
return Err(ExecutorError::UnsupportedExpression(format!(
"Cannot compute SUM on non-numeric value: {:?}",
value
)))
}
}
count += 1;
}
}
Ok(if count > 0 {
if has_float {
SqlValue::Double(float_sum)
} else {
SqlValue::Integer(int_sum)
}
} else {
SqlValue::Null
})
}
AggregateOp::Count => {
let mut count = 0;
for (row_idx, row) in rows.iter().enumerate() {
if let Some(bitmap) = filter_bitmap {
if !bitmap.get(row_idx).copied().unwrap_or(false) {
continue;
}
}
let value = eval_simple_expr(expr, row, schema)?;
if !matches!(value, SqlValue::Null) {
count += 1;
}
}
Ok(SqlValue::Integer(count))
}
AggregateOp::Avg => {
let sum_result =
compute_expression_aggregate(rows, expr, AggregateOp::Sum, filter_bitmap, schema)?;
let count_result = compute_expression_aggregate(
rows,
expr,
AggregateOp::Count,
filter_bitmap,
schema,
)?;
match (sum_result, count_result) {
(SqlValue::Numeric(sum), SqlValue::Integer(count)) if count > 0 => {
Ok(SqlValue::Double(sum / count as f64))
}
(SqlValue::Double(sum), SqlValue::Integer(count)) if count > 0 => {
Ok(SqlValue::Double(sum / count as f64))
}
(SqlValue::Integer(sum), SqlValue::Integer(count)) if count > 0 => {
Ok(SqlValue::Double(sum as f64 / count as f64))
}
_ => Ok(SqlValue::Null),
}
}
AggregateOp::Min | AggregateOp::Max => {
let mut result_value: Option<SqlValue> = None;
for (row_idx, row) in rows.iter().enumerate() {
if let Some(bitmap) = filter_bitmap {
if !bitmap.get(row_idx).copied().unwrap_or(false) {
continue;
}
}
let value = eval_simple_expr(expr, row, schema)?;
if !matches!(value, SqlValue::Null) {
result_value = Some(match &result_value {
None => value,
Some(current) => {
let should_update = if op == AggregateOp::Min {
compare_for_min_max(&value, current)
} else {
compare_for_min_max(current, &value)
};
if should_update {
value
} else {
current.clone()
}
}
});
}
}
Ok(result_value.unwrap_or(SqlValue::Null))
}
}
}