use ahash::AHashMap;
use vibesql_storage::Row;
use vibesql_types::SqlValue;
use super::{
super::{
batch::{ColumnArray, ColumnarBatch},
scan::ColumnarScan,
},
functions::compute_columnar_aggregate_impl,
AggregateOp,
};
use crate::{
errors::ExecutorError,
select::grouping::{GroupKey, GroupKeySpec},
};
#[inline]
fn sum_i64_indexed(values: &[i64], indices: &[usize], nulls: Option<&[bool]>) -> i64 {
let (mut s0, mut s1, mut s2, mut s3) = (0i64, 0i64, 0i64, 0i64);
let chunks = indices.len() / 4;
if let Some(null_mask) = nulls {
for i in 0..chunks {
let off = i * 4;
let (i0, i1, i2, i3) =
(indices[off], indices[off + 1], indices[off + 2], indices[off + 3]);
if !null_mask[i0] {
s0 = s0.wrapping_add(values[i0]);
}
if !null_mask[i1] {
s1 = s1.wrapping_add(values[i1]);
}
if !null_mask[i2] {
s2 = s2.wrapping_add(values[i2]);
}
if !null_mask[i3] {
s3 = s3.wrapping_add(values[i3]);
}
}
let mut sum = s0.wrapping_add(s1).wrapping_add(s2).wrapping_add(s3);
for &idx in &indices[chunks * 4..] {
if !null_mask[idx] {
sum = sum.wrapping_add(values[idx]);
}
}
sum
} else {
for i in 0..chunks {
let off = i * 4;
s0 = s0.wrapping_add(values[indices[off]]);
s1 = s1.wrapping_add(values[indices[off + 1]]);
s2 = s2.wrapping_add(values[indices[off + 2]]);
s3 = s3.wrapping_add(values[indices[off + 3]]);
}
let mut sum = s0.wrapping_add(s1).wrapping_add(s2).wrapping_add(s3);
for &idx in &indices[chunks * 4..] {
sum = sum.wrapping_add(values[idx]);
}
sum
}
}
#[inline]
fn sum_f64_indexed(values: &[f64], indices: &[usize], nulls: Option<&[bool]>) -> f64 {
let (mut s0, mut s1, mut s2, mut s3) = (0.0f64, 0.0f64, 0.0f64, 0.0f64);
let chunks = indices.len() / 4;
if let Some(null_mask) = nulls {
for i in 0..chunks {
let off = i * 4;
let (i0, i1, i2, i3) =
(indices[off], indices[off + 1], indices[off + 2], indices[off + 3]);
if !null_mask[i0] {
s0 += values[i0];
}
if !null_mask[i1] {
s1 += values[i1];
}
if !null_mask[i2] {
s2 += values[i2];
}
if !null_mask[i3] {
s3 += values[i3];
}
}
let mut sum = s0 + s1 + s2 + s3;
for &idx in &indices[chunks * 4..] {
if !null_mask[idx] {
sum += values[idx];
}
}
sum
} else {
for i in 0..chunks {
let off = i * 4;
s0 += values[indices[off]];
s1 += values[indices[off + 1]];
s2 += values[indices[off + 2]];
s3 += values[indices[off + 3]];
}
let mut sum = s0 + s1 + s2 + s3;
for &idx in &indices[chunks * 4..] {
sum += values[idx];
}
sum
}
}
#[inline]
fn count_indexed(indices: &[usize], nulls: Option<&[bool]>) -> usize {
if let Some(null_mask) = nulls {
indices.iter().filter(|&&idx| !null_mask[idx]).count()
} else {
indices.len()
}
}
#[inline]
fn min_i64_indexed(values: &[i64], indices: &[usize], nulls: Option<&[bool]>) -> Option<i64> {
let mut result = i64::MAX;
let mut found = false;
if let Some(null_mask) = nulls {
for &idx in indices {
if !null_mask[idx] {
result = result.min(values[idx]);
found = true;
}
}
} else {
for &idx in indices {
result = result.min(values[idx]);
found = true;
}
}
if found {
Some(result)
} else {
None
}
}
#[inline]
fn max_i64_indexed(values: &[i64], indices: &[usize], nulls: Option<&[bool]>) -> Option<i64> {
let mut result = i64::MIN;
let mut found = false;
if let Some(null_mask) = nulls {
for &idx in indices {
if !null_mask[idx] {
result = result.max(values[idx]);
found = true;
}
}
} else {
for &idx in indices {
result = result.max(values[idx]);
found = true;
}
}
if found {
Some(result)
} else {
None
}
}
#[inline]
fn min_f64_indexed(values: &[f64], indices: &[usize], nulls: Option<&[bool]>) -> Option<f64> {
let mut result = f64::INFINITY;
let mut found = false;
if let Some(null_mask) = nulls {
for &idx in indices {
if !null_mask[idx] {
result = result.min(values[idx]);
found = true;
}
}
} else {
for &idx in indices {
result = result.min(values[idx]);
found = true;
}
}
if found {
Some(result)
} else {
None
}
}
#[inline]
fn max_f64_indexed(values: &[f64], indices: &[usize], nulls: Option<&[bool]>) -> Option<f64> {
let mut result = f64::NEG_INFINITY;
let mut found = false;
if let Some(null_mask) = nulls {
for &idx in indices {
if !null_mask[idx] {
result = result.max(values[idx]);
found = true;
}
}
} else {
for &idx in indices {
result = result.max(values[idx]);
found = true;
}
}
if found {
Some(result)
} else {
None
}
}
fn compute_group_aggregate_indexed(
batch: &ColumnarBatch,
col_idx: usize,
op: AggregateOp,
indices: &[usize],
) -> Result<SqlValue, ExecutorError> {
let column = batch.column(col_idx).ok_or_else(|| ExecutorError::ColumnarColumnNotFound {
column_index: col_idx,
batch_columns: batch.column_count(),
})?;
match column {
ColumnArray::Int64(values, nulls) => {
let null_slice = nulls.as_ref().map(|v| v.as_slice());
let count = count_indexed(indices, null_slice);
if count == 0 {
return Ok(match op {
AggregateOp::Count => SqlValue::Integer(0),
_ => SqlValue::Null,
});
}
match op {
AggregateOp::Sum => {
Ok(SqlValue::Integer(sum_i64_indexed(values, indices, null_slice)))
}
AggregateOp::Count => Ok(SqlValue::Integer(count as i64)),
AggregateOp::Avg => {
let sum = sum_i64_indexed(values, indices, null_slice);
Ok(SqlValue::Double(sum as f64 / count as f64))
}
AggregateOp::Min => min_i64_indexed(values, indices, null_slice)
.map(SqlValue::Integer)
.ok_or_else(|| ExecutorError::SimdOperationFailed {
operation: "MIN".to_string(),
reason: "empty group".to_string(),
}),
AggregateOp::Max => max_i64_indexed(values, indices, null_slice)
.map(SqlValue::Integer)
.ok_or_else(|| ExecutorError::SimdOperationFailed {
operation: "MAX".to_string(),
reason: "empty group".to_string(),
}),
}
}
ColumnArray::Float64(values, nulls) => {
let null_slice = nulls.as_ref().map(|v| v.as_slice());
let count = count_indexed(indices, null_slice);
if count == 0 {
return Ok(match op {
AggregateOp::Count => SqlValue::Integer(0),
_ => SqlValue::Null,
});
}
match op {
AggregateOp::Sum => {
Ok(SqlValue::Numeric(sum_f64_indexed(values, indices, null_slice)))
}
AggregateOp::Count => Ok(SqlValue::Integer(count as i64)),
AggregateOp::Avg => {
let sum = sum_f64_indexed(values, indices, null_slice);
Ok(SqlValue::Double(sum / count as f64))
}
AggregateOp::Min => min_f64_indexed(values, indices, null_slice)
.map(SqlValue::Double)
.ok_or_else(|| ExecutorError::SimdOperationFailed {
operation: "MIN".to_string(),
reason: "empty group".to_string(),
}),
AggregateOp::Max => max_f64_indexed(values, indices, null_slice)
.map(SqlValue::Double)
.ok_or_else(|| ExecutorError::SimdOperationFailed {
operation: "MAX".to_string(),
reason: "empty group".to_string(),
}),
}
}
ColumnArray::Mixed(values) => {
fn sqlvalue_to_f64(v: &SqlValue) -> Option<f64> {
match v {
SqlValue::Integer(n) => Some(*n as f64),
SqlValue::Bigint(n) => Some(*n as f64),
SqlValue::Smallint(n) => Some(*n as f64),
SqlValue::Float(n) => Some(*n as f64),
SqlValue::Double(n) => Some(*n),
SqlValue::Numeric(n) => Some(*n),
SqlValue::Real(n) => Some(*n as f64),
_ => None,
}
}
let count = indices.len();
if count == 0 {
return Ok(match op {
AggregateOp::Count => SqlValue::Integer(0),
_ => SqlValue::Null,
});
}
match op {
AggregateOp::Count => {
let non_null = indices.iter().filter(|&&i| !values[i].is_null()).count();
Ok(SqlValue::Integer(non_null as i64))
}
AggregateOp::Sum => {
let mut sum = 0.0f64;
for &idx in indices {
if let Some(v) = sqlvalue_to_f64(&values[idx]) {
sum += v;
}
}
Ok(SqlValue::Double(sum))
}
AggregateOp::Avg => {
let mut sum = 0.0f64;
let mut cnt = 0usize;
for &idx in indices {
if let Some(v) = sqlvalue_to_f64(&values[idx]) {
sum += v;
cnt += 1;
}
}
if cnt > 0 {
Ok(SqlValue::Double(sum / cnt as f64))
} else {
Ok(SqlValue::Null)
}
}
AggregateOp::Min => {
let mut min_val: Option<f64> = None;
for &idx in indices {
if let Some(v) = sqlvalue_to_f64(&values[idx]) {
min_val = Some(min_val.map_or(v, |m| m.min(v)));
}
}
Ok(min_val.map(SqlValue::Double).unwrap_or(SqlValue::Null))
}
AggregateOp::Max => {
let mut max_val: Option<f64> = None;
for &idx in indices {
if let Some(v) = sqlvalue_to_f64(&values[idx]) {
max_val = Some(max_val.map_or(v, |m| m.max(v)));
}
}
Ok(max_val.map(SqlValue::Double).unwrap_or(SqlValue::Null))
}
}
}
ColumnArray::String(_values, nulls) => {
let null_slice = nulls.as_ref().map(|v| v.as_slice());
let count = count_indexed(indices, null_slice);
match op {
AggregateOp::Count => Ok(SqlValue::Integer(count as i64)),
_ => Err(ExecutorError::UnsupportedExpression(format!(
"Aggregate operation {:?} not supported for String column type",
op
))),
}
}
ColumnArray::FixedString(_values, nulls) => {
let null_slice = nulls.as_ref().map(|v| v.as_slice());
let count = count_indexed(indices, null_slice);
match op {
AggregateOp::Count => Ok(SqlValue::Integer(count as i64)),
_ => Err(ExecutorError::UnsupportedExpression(format!(
"Aggregate operation {:?} not supported for FixedString column type",
op
))),
}
}
ColumnArray::Boolean(_values, nulls) => {
let null_slice = nulls.as_ref().map(|v| v.as_slice());
let count = count_indexed(indices, null_slice);
match op {
AggregateOp::Count => Ok(SqlValue::Integer(count as i64)),
_ => Err(ExecutorError::UnsupportedExpression(format!(
"Aggregate operation {:?} not supported for Boolean column type",
op
))),
}
}
ColumnArray::Int32(values, nulls) => {
let null_slice = nulls.as_ref().map(|v| v.as_slice());
let count = count_indexed(indices, null_slice);
if count == 0 {
return Ok(match op {
AggregateOp::Count => SqlValue::Integer(0),
_ => SqlValue::Null,
});
}
match op {
AggregateOp::Count => Ok(SqlValue::Integer(count as i64)),
AggregateOp::Sum => {
let sum: i64 = indices
.iter()
.filter(|&&i| null_slice.is_none_or(|ns| !ns[i]))
.map(|&i| values[i] as i64)
.sum();
Ok(SqlValue::Integer(sum))
}
AggregateOp::Avg => {
let sum: i64 = indices
.iter()
.filter(|&&i| null_slice.is_none_or(|ns| !ns[i]))
.map(|&i| values[i] as i64)
.sum();
Ok(SqlValue::Double(sum as f64 / count as f64))
}
AggregateOp::Min => indices
.iter()
.filter(|&&i| null_slice.is_none_or(|ns| !ns[i]))
.map(|&i| values[i])
.min()
.map(|v| SqlValue::Integer(v as i64))
.ok_or_else(|| ExecutorError::SimdOperationFailed {
operation: "MIN".to_string(),
reason: "empty group".to_string(),
}),
AggregateOp::Max => indices
.iter()
.filter(|&&i| null_slice.is_none_or(|ns| !ns[i]))
.map(|&i| values[i])
.max()
.map(|v| SqlValue::Integer(v as i64))
.ok_or_else(|| ExecutorError::SimdOperationFailed {
operation: "MAX".to_string(),
reason: "empty group".to_string(),
}),
}
}
ColumnArray::Float32(values, nulls) => {
let null_slice = nulls.as_ref().map(|v| v.as_slice());
let count = count_indexed(indices, null_slice);
if count == 0 {
return Ok(match op {
AggregateOp::Count => SqlValue::Integer(0),
_ => SqlValue::Null,
});
}
match op {
AggregateOp::Count => Ok(SqlValue::Integer(count as i64)),
AggregateOp::Sum => {
let sum: f64 = indices
.iter()
.filter(|&&i| null_slice.is_none_or(|ns| !ns[i]))
.map(|&i| values[i] as f64)
.sum();
Ok(SqlValue::Double(sum))
}
AggregateOp::Avg => {
let sum: f64 = indices
.iter()
.filter(|&&i| null_slice.is_none_or(|ns| !ns[i]))
.map(|&i| values[i] as f64)
.sum();
Ok(SqlValue::Double(sum / count as f64))
}
AggregateOp::Min => indices
.iter()
.filter(|&&i| null_slice.is_none_or(|ns| !ns[i]))
.map(|&i| values[i])
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|v| SqlValue::Double(v as f64))
.ok_or_else(|| ExecutorError::SimdOperationFailed {
operation: "MIN".to_string(),
reason: "empty group".to_string(),
}),
AggregateOp::Max => indices
.iter()
.filter(|&&i| null_slice.is_none_or(|ns| !ns[i]))
.map(|&i| values[i])
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|v| SqlValue::Double(v as f64))
.ok_or_else(|| ExecutorError::SimdOperationFailed {
operation: "MAX".to_string(),
reason: "empty group".to_string(),
}),
}
}
ColumnArray::Date(_values, nulls) => {
let null_slice = nulls.as_ref().map(|v| v.as_slice());
let count = count_indexed(indices, null_slice);
match op {
AggregateOp::Count => Ok(SqlValue::Integer(count as i64)),
_ => Err(ExecutorError::UnsupportedExpression(format!(
"Aggregate operation {:?} not supported for Date column type in GROUP BY",
op
))),
}
}
ColumnArray::Timestamp(_values, nulls) => {
let null_slice = nulls.as_ref().map(|v| v.as_slice());
let count = count_indexed(indices, null_slice);
match op {
AggregateOp::Count => Ok(SqlValue::Integer(count as i64)),
_ => Err(ExecutorError::UnsupportedExpression(format!(
"Aggregate operation {:?} not supported for Timestamp column type in GROUP BY",
op
))),
}
}
}
}
pub fn columnar_group_by(
rows: &[Row],
group_cols: &[usize],
agg_cols: &[(usize, AggregateOp)],
filter_bitmap: Option<&[bool]>,
) -> Result<Vec<Row>, ExecutorError> {
if rows.is_empty() {
return Ok(Vec::new());
}
let scan = ColumnarScan::new(rows);
let mut groups: AHashMap<Vec<SqlValue>, Vec<usize>> = AHashMap::new();
for row_idx in 0..rows.len() {
if let Some(bitmap) = filter_bitmap {
if !bitmap.get(row_idx).copied().unwrap_or(false) {
continue;
}
}
let mut group_key = Vec::with_capacity(group_cols.len());
for &col_idx in group_cols {
let value =
scan.row(row_idx).and_then(|row| row.get(col_idx)).unwrap_or(&SqlValue::Null);
group_key.push(value.clone());
}
groups.entry(group_key).or_default().push(row_idx);
}
let mut result_rows = Vec::with_capacity(groups.len());
let mut group_bitmap = vec![false; rows.len()];
for (group_key, row_indices) in groups {
for &idx in &row_indices {
group_bitmap[idx] = true;
}
let mut result_values = Vec::with_capacity(group_key.len() + agg_cols.len());
result_values.extend(group_key);
for (col_idx, agg_op) in agg_cols {
let agg_result =
compute_columnar_aggregate_impl(&scan, *col_idx, *agg_op, Some(&group_bitmap))?;
result_values.push(agg_result);
}
result_rows.push(Row::new(result_values));
for &idx in &row_indices {
group_bitmap[idx] = false;
}
}
sort_rows_by_group_key_prefix(&mut result_rows, group_cols.len());
Ok(result_rows)
}
fn sort_rows_by_group_key_prefix(rows: &mut [Row], group_key_len: usize) {
if group_key_len == 0 {
return;
}
rows.sort_by(|a, b| {
let a_key = &a.values[..group_key_len.min(a.values.len())];
let b_key = &b.values[..group_key_len.min(b.values.len())];
a_key.cmp(b_key)
});
}
pub fn columnar_group_by_batch(
batch: &ColumnarBatch,
group_cols: &[usize],
agg_cols: &[(usize, AggregateOp)],
) -> Result<Vec<Row>, ExecutorError> {
if batch.row_count() == 0 {
return Ok(Vec::new());
}
let row_count = batch.row_count();
let key_spec = GroupKeySpec::from_columnar_batch(batch, group_cols);
let mut groups: AHashMap<GroupKey, Vec<usize>> = AHashMap::new();
for row_idx in 0..row_count {
let group_key = key_spec.extract_key_from_batch(batch, row_idx);
groups.entry(group_key).or_default().push(row_idx);
}
let mut result_rows = Vec::with_capacity(groups.len());
for (group_key, row_indices) in groups {
let mut result_values = key_spec.key_to_values(&group_key);
for (col_idx, agg_op) in agg_cols {
let agg_result =
compute_group_aggregate_indexed(batch, *col_idx, *agg_op, &row_indices)?;
result_values.push(agg_result);
}
result_rows.push(Row::new(result_values));
}
sort_rows_by_group_key_prefix(&mut result_rows, group_cols.len());
Ok(result_rows)
}
#[cfg(test)]
mod batch_tests {
use super::*;
fn make_test_batch() -> ColumnarBatch {
let rows = vec![
Row::new(vec![SqlValue::Varchar(arcstr::ArcStr::from("A")), SqlValue::Integer(10)]),
Row::new(vec![SqlValue::Varchar(arcstr::ArcStr::from("B")), SqlValue::Integer(20)]),
Row::new(vec![SqlValue::Varchar(arcstr::ArcStr::from("A")), SqlValue::Integer(30)]),
Row::new(vec![SqlValue::Varchar(arcstr::ArcStr::from("B")), SqlValue::Integer(40)]),
Row::new(vec![SqlValue::Varchar(arcstr::ArcStr::from("A")), SqlValue::Integer(50)]),
];
ColumnarBatch::from_rows(&rows).unwrap()
}
#[test]
fn test_columnar_group_by_batch_sum() {
let batch = make_test_batch();
let group_cols = vec![0]; let agg_cols = vec![(1, AggregateOp::Sum)];
let result = columnar_group_by_batch(&batch, &group_cols, &agg_cols).unwrap();
assert_eq!(result.len(), 2);
let mut sorted = result;
sorted.sort_by(|a, b| a.get(0).unwrap().partial_cmp(b.get(0).unwrap()).unwrap());
assert_eq!(sorted[0].get(0), Some(&SqlValue::Varchar(arcstr::ArcStr::from("A"))));
assert_eq!(sorted[0].get(1), Some(&SqlValue::Integer(90)));
assert_eq!(sorted[1].get(0), Some(&SqlValue::Varchar(arcstr::ArcStr::from("B"))));
assert_eq!(sorted[1].get(1), Some(&SqlValue::Integer(60)));
}
#[test]
fn test_columnar_group_by_batch_avg() {
let batch = make_test_batch();
let group_cols = vec![0];
let agg_cols = vec![(1, AggregateOp::Avg)];
let result = columnar_group_by_batch(&batch, &group_cols, &agg_cols).unwrap();
let mut sorted = result;
sorted.sort_by(|a, b| a.get(0).unwrap().partial_cmp(b.get(0).unwrap()).unwrap());
assert_eq!(sorted[0].get(0), Some(&SqlValue::Varchar(arcstr::ArcStr::from("A"))));
if let Some(SqlValue::Double(avg)) = sorted[0].get(1) {
assert!((avg - 30.0).abs() < 0.001);
} else {
panic!("Expected Double for AVG");
}
assert_eq!(sorted[1].get(0), Some(&SqlValue::Varchar(arcstr::ArcStr::from("B"))));
if let Some(SqlValue::Double(avg)) = sorted[1].get(1) {
assert!((avg - 30.0).abs() < 0.001);
} else {
panic!("Expected Double for AVG");
}
}
#[test]
fn test_columnar_group_by_batch_min_max() {
let batch = make_test_batch();
let group_cols = vec![0];
let agg_cols = vec![(1, AggregateOp::Min), (1, AggregateOp::Max)];
let result = columnar_group_by_batch(&batch, &group_cols, &agg_cols).unwrap();
let mut sorted = result;
sorted.sort_by(|a, b| a.get(0).unwrap().partial_cmp(b.get(0).unwrap()).unwrap());
assert_eq!(sorted[0].get(1), Some(&SqlValue::Integer(10)));
assert_eq!(sorted[0].get(2), Some(&SqlValue::Integer(50)));
assert_eq!(sorted[1].get(1), Some(&SqlValue::Integer(20)));
assert_eq!(sorted[1].get(2), Some(&SqlValue::Integer(40)));
}
#[test]
fn test_columnar_group_by_batch_count() {
let batch = make_test_batch();
let group_cols = vec![0];
let agg_cols = vec![(1, AggregateOp::Count)];
let result = columnar_group_by_batch(&batch, &group_cols, &agg_cols).unwrap();
let mut sorted = result;
sorted.sort_by(|a, b| a.get(0).unwrap().partial_cmp(b.get(0).unwrap()).unwrap());
assert_eq!(sorted[0].get(1), Some(&SqlValue::Integer(3)));
assert_eq!(sorted[1].get(1), Some(&SqlValue::Integer(2)));
}
#[test]
fn test_columnar_group_by_batch_empty() {
let batch = ColumnarBatch::new(2);
let group_cols = vec![0];
let agg_cols = vec![(1, AggregateOp::Sum)];
let result = columnar_group_by_batch(&batch, &group_cols, &agg_cols).unwrap();
assert_eq!(result.len(), 0);
}
fn make_unsorted_key_rows() -> Vec<Row> {
let keys = ["W", "Z", "Q", "A", "M", "X", "B", "Y", "K", "C"];
let mut rows = Vec::new();
for (i, key) in keys.iter().enumerate() {
rows.push(Row::new(vec![
SqlValue::Varchar(arcstr::ArcStr::from(*key)),
SqlValue::Integer(i as i64),
]));
rows.push(Row::new(vec![
SqlValue::Varchar(arcstr::ArcStr::from(*key)),
SqlValue::Integer((i * 10) as i64),
]));
}
rows
}
fn assert_sorted_by_first_col(result: &[Row]) {
for pair in result.windows(2) {
assert!(
pair[0].values[0] <= pair[1].values[0],
"result not sorted by group key: {:?} > {:?}",
pair[0].values[0],
pair[1].values[0]
);
}
}
#[test]
fn test_columnar_group_by_deterministic_key_order() {
let rows = make_unsorted_key_rows();
let group_cols = vec![0];
let agg_cols = vec![(1, AggregateOp::Sum)];
let first = columnar_group_by(&rows, &group_cols, &agg_cols, None).unwrap();
assert_eq!(first.len(), 10);
assert_sorted_by_first_col(&first);
for run in 0..10 {
let again = columnar_group_by(&rows, &group_cols, &agg_cols, None).unwrap();
assert_eq!(
again.iter().map(|r| r.values.to_vec()).collect::<Vec<_>>(),
first.iter().map(|r| r.values.to_vec()).collect::<Vec<_>>(),
"run {} produced different row order",
run
);
}
}
#[test]
fn test_columnar_group_by_batch_deterministic_key_order() {
let rows = make_unsorted_key_rows();
let batch = ColumnarBatch::from_rows(&rows).unwrap();
let group_cols = vec![0];
let agg_cols = vec![(1, AggregateOp::Sum)];
let first = columnar_group_by_batch(&batch, &group_cols, &agg_cols).unwrap();
assert_eq!(first.len(), 10);
assert_sorted_by_first_col(&first);
for run in 0..10 {
let again = columnar_group_by_batch(&batch, &group_cols, &agg_cols).unwrap();
assert_eq!(
again.iter().map(|r| r.values.to_vec()).collect::<Vec<_>>(),
first.iter().map(|r| r.values.to_vec()).collect::<Vec<_>>(),
"run {} produced different row order",
run
);
}
}
#[test]
fn test_columnar_group_by_multi_key_order() {
let rows = vec![
Row::new(vec![
SqlValue::Varchar(arcstr::ArcStr::from("B")),
SqlValue::Integer(2),
SqlValue::Integer(1),
]),
Row::new(vec![
SqlValue::Varchar(arcstr::ArcStr::from("A")),
SqlValue::Integer(2),
SqlValue::Integer(2),
]),
Row::new(vec![
SqlValue::Varchar(arcstr::ArcStr::from("B")),
SqlValue::Integer(1),
SqlValue::Integer(3),
]),
Row::new(vec![
SqlValue::Varchar(arcstr::ArcStr::from("A")),
SqlValue::Integer(1),
SqlValue::Integer(4),
]),
];
let group_cols = vec![0, 1];
let agg_cols = vec![(2, AggregateOp::Sum)];
let result = columnar_group_by(&rows, &group_cols, &agg_cols, None).unwrap();
let keys: Vec<(String, i64)> = result
.iter()
.map(|r| {
let s = match &r.values[0] {
SqlValue::Varchar(s) => s.to_string(),
other => panic!("unexpected key type: {:?}", other),
};
let n = match &r.values[1] {
SqlValue::Integer(n) => *n,
other => panic!("unexpected key type: {:?}", other),
};
(s, n)
})
.collect();
assert_eq!(
keys,
vec![
("A".to_string(), 1),
("A".to_string(), 2),
("B".to_string(), 1),
("B".to_string(), 2),
]
);
}
}