mod expression;
mod functions;
mod group_by;
pub use expression::{
evaluate_expression_to_column, evaluate_expression_with_cached_column, extract_aggregates,
};
pub use group_by::columnar_group_by;
pub use group_by::columnar_group_by_batch;
use vibesql_ast::Expression;
use vibesql_storage::Row;
use vibesql_types::SqlValue;
use super::{batch::ColumnarBatch, scan::ColumnarScan};
use crate::{errors::ExecutorError, schema::CombinedSchema};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AggregateOp {
Sum,
Count,
Avg,
Min,
Max,
}
#[derive(Debug, Clone, PartialEq)]
pub enum AggregateSource {
Column(usize),
Expression(Expression),
CountStar,
}
#[derive(Debug, Clone)]
pub struct AggregateSpec {
pub op: AggregateOp,
pub source: AggregateSource,
}
pub fn compute_columnar_aggregate(
scan: &ColumnarScan,
column_idx: usize,
op: AggregateOp,
filter_bitmap: Option<&[bool]>,
) -> Result<SqlValue, ExecutorError> {
{
use super::simd_aggregate::{
can_use_simd_for_column, simd_aggregate_f64, simd_aggregate_i64,
};
if let Some(is_integer) = can_use_simd_for_column(scan, column_idx) {
return if is_integer {
simd_aggregate_i64(scan, column_idx, op, filter_bitmap)
} else {
simd_aggregate_f64(scan, column_idx, op, filter_bitmap)
};
}
}
functions::compute_columnar_aggregate_impl(scan, column_idx, op, filter_bitmap)
}
pub fn compute_multiple_aggregates(
rows: &[Row],
aggregates: &[AggregateSpec],
filter_bitmap: Option<&[bool]>,
schema: Option<&CombinedSchema>,
) -> Result<Vec<SqlValue>, ExecutorError> {
let scan = ColumnarScan::new(rows);
let mut results = Vec::with_capacity(aggregates.len());
for spec in aggregates {
let result = match &spec.source {
AggregateSource::Column(column_idx) => {
compute_columnar_aggregate(&scan, *column_idx, spec.op, filter_bitmap)?
}
AggregateSource::Expression(expr) => {
let schema = schema.ok_or_else(|| {
ExecutorError::UnsupportedExpression(
"Schema required for expression aggregates".to_string(),
)
})?;
expression::compute_expression_aggregate(
rows,
expr,
spec.op,
filter_bitmap,
schema,
)?
}
AggregateSource::CountStar => functions::compute_count(&scan, filter_bitmap)?,
};
results.push(result);
}
Ok(results)
}
pub fn compute_aggregates_from_batch(
batch: &ColumnarBatch,
aggregates: &[AggregateSpec],
schema: Option<&CombinedSchema>,
) -> Result<Vec<SqlValue>, ExecutorError> {
if batch.row_count() == 0 {
return Ok(aggregates
.iter()
.map(|spec| match spec.op {
AggregateOp::Count => SqlValue::Integer(0),
_ => SqlValue::Null,
})
.collect());
}
let mut results = Vec::with_capacity(aggregates.len());
for spec in aggregates {
let result = match &spec.source {
AggregateSource::Column(column_idx) => {
functions::compute_batch_aggregate(batch, *column_idx, spec.op)?
}
AggregateSource::Expression(expr) => {
let schema = schema.ok_or_else(|| {
ExecutorError::UnsupportedExpression(
"Schema required for expression aggregates".to_string(),
)
})?;
match expression::compute_batch_expression_aggregate(batch, expr, spec.op, schema) {
Ok(value) => value,
Err(ExecutorError::UnsupportedExpression(_)) => {
let rows = batch.to_rows()?;
expression::compute_expression_aggregate(
&rows, expr, spec.op, None, schema,
)?
}
Err(other) => return Err(other),
}
}
AggregateSource::CountStar => SqlValue::Integer(batch.row_count() as i64),
};
results.push(result);
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_test_rows() -> Vec<Row> {
vec![
Row::new(vec![SqlValue::Integer(10), SqlValue::Double(1.5)]),
Row::new(vec![SqlValue::Integer(20), SqlValue::Double(2.5)]),
Row::new(vec![SqlValue::Integer(30), SqlValue::Double(3.5)]),
]
}
#[test]
fn test_sum_aggregate() {
let rows = make_test_rows();
let scan = ColumnarScan::new(&rows);
let result = functions::compute_sum(&scan, 0, None).unwrap();
assert_eq!(result, SqlValue::Integer(60));
let result = functions::compute_sum(&scan, 1, None).unwrap();
assert!(matches!(result, SqlValue::Double(sum) if (sum - 7.5).abs() < 0.001));
}
#[test]
fn test_count_aggregate() {
let rows = make_test_rows();
let scan = ColumnarScan::new(&rows);
let result = functions::compute_count(&scan, None).unwrap();
assert_eq!(result, SqlValue::Integer(3));
}
#[test]
fn test_sum_with_filter() {
let rows = make_test_rows();
let scan = ColumnarScan::new(&rows);
let filter = vec![true, false, true];
let result = functions::compute_sum(&scan, 0, Some(&filter)).unwrap();
assert_eq!(result, SqlValue::Integer(40));
}
#[test]
fn test_multiple_aggregates() {
let rows = make_test_rows();
let aggregates = vec![
AggregateSpec { op: AggregateOp::Sum, source: AggregateSource::Column(0) },
AggregateSpec { op: AggregateOp::Avg, source: AggregateSource::Column(1) },
];
let results = compute_multiple_aggregates(&rows, &aggregates, None, None).unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0], SqlValue::Integer(60));
assert!(matches!(results[1], SqlValue::Double(avg) if (avg - 2.5).abs() < 0.001));
}
#[test]
fn test_extract_aggregates_simple() {
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
use crate::schema::CombinedSchema;
let schema = TableSchema::new(
"test".to_string(),
vec![
ColumnSchema::new("col1".to_string(), DataType::Integer, false),
ColumnSchema::new("col2".to_string(), DataType::DoublePrecision, false),
],
);
let combined_schema = CombinedSchema::from_table("test".to_string(), schema);
let exprs = vec![Expression::AggregateFunction {
name: vibesql_ast::FunctionIdentifier::new("SUM"),
distinct: false,
args: vec![Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple("col1", false))],
order_by: None,
filter: None,
}];
let result = extract_aggregates(&exprs, &combined_schema);
assert!(result.is_some());
let aggregates = result.unwrap();
assert_eq!(aggregates.len(), 1);
assert!(matches!(aggregates[0].op, AggregateOp::Sum));
assert!(matches!(aggregates[0].source, AggregateSource::Column(0)));
let exprs = vec![Expression::AggregateFunction {
name: vibesql_ast::FunctionIdentifier::new("COUNT"),
distinct: false,
args: vec![Expression::Wildcard],
order_by: None,
filter: None,
}];
let result = extract_aggregates(&exprs, &combined_schema);
assert!(result.is_some());
let aggregates = result.unwrap();
assert_eq!(aggregates.len(), 1);
assert!(matches!(aggregates[0].op, AggregateOp::Count));
assert!(matches!(aggregates[0].source, AggregateSource::CountStar));
let exprs = vec![
Expression::AggregateFunction {
name: vibesql_ast::FunctionIdentifier::new("SUM"),
distinct: false,
args: vec![Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"col1", false,
))],
order_by: None,
filter: None,
},
Expression::AggregateFunction {
name: vibesql_ast::FunctionIdentifier::new("AVG"),
distinct: false,
args: vec![Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"col2", false,
))],
order_by: None,
filter: None,
},
];
let result = extract_aggregates(&exprs, &combined_schema);
assert!(result.is_some());
let aggregates = result.unwrap();
assert_eq!(aggregates.len(), 2);
assert!(matches!(aggregates[0].op, AggregateOp::Sum));
assert!(matches!(aggregates[0].source, AggregateSource::Column(0)));
assert!(matches!(aggregates[1].op, AggregateOp::Avg));
assert!(matches!(aggregates[1].source, AggregateSource::Column(1)));
}
#[test]
fn test_extract_aggregates_unsupported() {
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
use crate::schema::CombinedSchema;
let schema = TableSchema::new(
"test".to_string(),
vec![ColumnSchema::new("col1".to_string(), DataType::Integer, false)],
);
let combined_schema = CombinedSchema::from_table("test".to_string(), schema);
let exprs = vec![Expression::AggregateFunction {
name: vibesql_ast::FunctionIdentifier::new("SUM"),
distinct: true,
args: vec![Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple("col1", false))],
order_by: None,
filter: None,
}];
let result = extract_aggregates(&exprs, &combined_schema);
assert!(result.is_none());
let exprs =
vec![Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple("col1", false))];
let result = extract_aggregates(&exprs, &combined_schema);
assert!(result.is_none());
let exprs = vec![Expression::AggregateFunction {
name: vibesql_ast::FunctionIdentifier::new("SUM"),
distinct: false,
args: vec![Expression::ScalarSubquery(Box::new(vibesql_ast::SelectStmt {
with_clause: None,
distinct: false,
select_list: vec![],
into_table: None,
into_variables: None,
from: None,
where_clause: None,
group_by: None,
having: None,
window_definitions: None,
order_by: None,
limit: None,
offset: None,
set_operation: None,
values: None,
}))],
order_by: None,
filter: None,
}];
let result = extract_aggregates(&exprs, &combined_schema);
assert!(result.is_none());
}
#[test]
fn test_extract_aggregates_with_expression() {
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
use crate::schema::CombinedSchema;
let schema = TableSchema::new(
"test".to_string(),
vec![
ColumnSchema::new("price".to_string(), DataType::DoublePrecision, false),
ColumnSchema::new("discount".to_string(), DataType::DoublePrecision, false),
],
);
let combined_schema = CombinedSchema::from_table("test".to_string(), schema);
let exprs = vec![Expression::AggregateFunction {
name: vibesql_ast::FunctionIdentifier::new("SUM"),
distinct: false,
args: vec![Expression::BinaryOp {
left: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"price", false,
))),
op: vibesql_ast::BinaryOperator::Multiply,
right: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"discount", false,
))),
}],
order_by: None,
filter: None,
}];
let result = extract_aggregates(&exprs, &combined_schema);
assert!(result.is_some());
let aggregates = result.unwrap();
assert_eq!(aggregates.len(), 1);
assert!(matches!(aggregates[0].op, AggregateOp::Sum));
assert!(matches!(aggregates[0].source, AggregateSource::Expression(_)));
}
#[test]
fn test_columnar_group_by_simple() {
let rows = vec![
Row::new(vec![SqlValue::Varchar(arcstr::ArcStr::from("A")), SqlValue::Double(100.0)]),
Row::new(vec![SqlValue::Varchar(arcstr::ArcStr::from("B")), SqlValue::Double(200.0)]),
Row::new(vec![SqlValue::Varchar(arcstr::ArcStr::from("A")), SqlValue::Double(150.0)]),
Row::new(vec![SqlValue::Varchar(arcstr::ArcStr::from("B")), SqlValue::Double(50.0)]),
];
let group_cols = vec![0]; let agg_cols = vec![(1, AggregateOp::Sum)];
let result = columnar_group_by(&rows, &group_cols, &agg_cols, None).unwrap();
assert_eq!(result.len(), 2);
let mut sorted = result;
sorted.sort_by(|a, b| {
let a_key = a.get(0).unwrap();
let b_key = b.get(0).unwrap();
a_key.partial_cmp(b_key).unwrap()
});
assert_eq!(sorted[0].get(0), Some(&SqlValue::Varchar(arcstr::ArcStr::from("A"))));
assert!(
matches!(sorted[0].get(1), Some(&SqlValue::Double(sum)) if (sum - 250.0).abs() < 0.001)
);
assert_eq!(sorted[1].get(0), Some(&SqlValue::Varchar(arcstr::ArcStr::from("B"))));
assert!(
matches!(sorted[1].get(1), Some(&SqlValue::Double(sum)) if (sum - 250.0).abs() < 0.001)
);
}
#[test]
fn test_columnar_group_by_multiple_group_keys() {
let rows = vec![
Row::new(vec![SqlValue::Varchar(arcstr::ArcStr::from("A")), SqlValue::Integer(1)]),
Row::new(vec![SqlValue::Varchar(arcstr::ArcStr::from("A")), SqlValue::Integer(2)]),
Row::new(vec![SqlValue::Varchar(arcstr::ArcStr::from("B")), SqlValue::Integer(1)]),
Row::new(vec![SqlValue::Varchar(arcstr::ArcStr::from("A")), SqlValue::Integer(1)]),
Row::new(vec![SqlValue::Varchar(arcstr::ArcStr::from("B")), SqlValue::Integer(2)]),
];
let group_cols = vec![0, 1]; let agg_cols = vec![(0, AggregateOp::Count)];
let result = columnar_group_by(&rows, &group_cols, &agg_cols, None).unwrap();
assert_eq!(result.len(), 4);
for row in &result {
let status = row.get(0).unwrap();
let category = row.get(1).unwrap();
let count = row.get(2).unwrap();
match (status, category) {
(SqlValue::Varchar(s), SqlValue::Integer(1)) if s.as_str() == "A" => {
assert_eq!(count, &SqlValue::Integer(2)); }
(SqlValue::Varchar(s), SqlValue::Integer(2)) if s.as_str() == "A" => {
assert_eq!(count, &SqlValue::Integer(1)); }
(SqlValue::Varchar(s), SqlValue::Integer(1)) if s.as_str() == "B" => {
assert_eq!(count, &SqlValue::Integer(1)); }
(SqlValue::Varchar(s), SqlValue::Integer(2)) if s.as_str() == "B" => {
assert_eq!(count, &SqlValue::Integer(1)); }
_ => panic!("Unexpected group key: {:?}, {:?}", status, category),
}
}
}
#[test]
fn test_columnar_group_by_multiple_aggregates() {
let rows = vec![
Row::new(vec![SqlValue::Integer(1), SqlValue::Double(100.0), SqlValue::Integer(10)]),
Row::new(vec![SqlValue::Integer(2), SqlValue::Double(200.0), SqlValue::Integer(20)]),
Row::new(vec![SqlValue::Integer(1), SqlValue::Double(150.0), SqlValue::Integer(15)]),
];
let group_cols = vec![0]; let agg_cols = vec![
(1, AggregateOp::Sum), (2, AggregateOp::Avg), (0, AggregateOp::Count), ];
let result = columnar_group_by(&rows, &group_cols, &agg_cols, None).unwrap();
assert_eq!(result.len(), 2);
let mut sorted = result;
sorted.sort_by(|a, b| {
let a_key = a.get(0).unwrap();
let b_key = b.get(0).unwrap();
a_key.partial_cmp(b_key).unwrap()
});
assert_eq!(sorted[0].get(0), Some(&SqlValue::Integer(1)));
assert!(
matches!(sorted[0].get(1), Some(&SqlValue::Double(sum)) if (sum - 250.0).abs() < 0.001)
);
assert!(
matches!(sorted[0].get(2), Some(&SqlValue::Double(avg)) if (avg - 12.5).abs() < 0.001)
);
assert_eq!(sorted[0].get(3), Some(&SqlValue::Integer(2)));
assert_eq!(sorted[1].get(0), Some(&SqlValue::Integer(2)));
assert!(
matches!(sorted[1].get(1), Some(&SqlValue::Double(sum)) if (sum - 200.0).abs() < 0.001)
);
assert!(
matches!(sorted[1].get(2), Some(&SqlValue::Double(avg)) if (avg - 20.0).abs() < 0.001)
);
assert_eq!(sorted[1].get(3), Some(&SqlValue::Integer(1)));
}
#[test]
fn test_columnar_group_by_with_filter() {
let rows = vec![
Row::new(vec![SqlValue::Varchar(arcstr::ArcStr::from("A")), SqlValue::Double(100.0)]),
Row::new(vec![SqlValue::Varchar(arcstr::ArcStr::from("B")), SqlValue::Double(200.0)]),
Row::new(vec![SqlValue::Varchar(arcstr::ArcStr::from("A")), SqlValue::Double(150.0)]),
Row::new(vec![SqlValue::Varchar(arcstr::ArcStr::from("B")), SqlValue::Double(50.0)]),
];
let filter = vec![false, true, true, false];
let group_cols = vec![0]; let agg_cols = vec![(1, AggregateOp::Sum)];
let result = columnar_group_by(&rows, &group_cols, &agg_cols, Some(&filter)).unwrap();
assert_eq!(result.len(), 2);
let mut sorted = result;
sorted.sort_by(|a, b| {
let a_key = a.get(0).unwrap();
let b_key = b.get(0).unwrap();
a_key.partial_cmp(b_key).unwrap()
});
assert_eq!(sorted[0].get(0), Some(&SqlValue::Varchar(arcstr::ArcStr::from("A"))));
assert!(
matches!(sorted[0].get(1), Some(&SqlValue::Double(sum)) if (sum - 150.0).abs() < 0.001)
);
assert_eq!(sorted[1].get(0), Some(&SqlValue::Varchar(arcstr::ArcStr::from("B"))));
assert!(
matches!(sorted[1].get(1), Some(&SqlValue::Double(sum)) if (sum - 200.0).abs() < 0.001)
);
}
#[test]
fn test_columnar_group_by_empty_input() {
let rows: Vec<Row> = vec![];
let group_cols = vec![0];
let agg_cols = vec![(1, AggregateOp::Sum)];
let result = columnar_group_by(&rows, &group_cols, &agg_cols, None).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_columnar_group_by_null_in_group_key() {
let rows = vec![
Row::new(vec![SqlValue::Varchar(arcstr::ArcStr::from("A")), SqlValue::Double(100.0)]),
Row::new(vec![SqlValue::Null, SqlValue::Double(200.0)]),
Row::new(vec![SqlValue::Varchar(arcstr::ArcStr::from("A")), SqlValue::Double(150.0)]),
Row::new(vec![SqlValue::Null, SqlValue::Double(50.0)]),
];
let group_cols = vec![0]; let agg_cols = vec![(1, AggregateOp::Sum)];
let result = columnar_group_by(&rows, &group_cols, &agg_cols, None).unwrap();
assert_eq!(result.len(), 2);
let a_group = result
.iter()
.find(|r| matches!(r.get(0), Some(SqlValue::Varchar(s)) if s.as_str() == "A"));
let null_group = result.iter().find(|r| matches!(r.get(0), Some(SqlValue::Null)));
assert!(a_group.is_some());
assert!(null_group.is_some());
assert!(
matches!(a_group.unwrap().get(1), Some(&SqlValue::Double(sum)) if (sum - 250.0).abs() < 0.001)
);
assert!(
matches!(null_group.unwrap().get(1), Some(&SqlValue::Double(sum)) if (sum - 250.0).abs() < 0.001)
);
}
}