Skip to main content

oxigdal_query/executor/
aggregate.rs

1//! Aggregation executor.
2
3use crate::error::{QueryError, Result};
4use crate::executor::filter::{Value, evaluate_expr_for_row};
5use crate::executor::scan::{ColumnData, DataType, Field, RecordBatch, Schema};
6use crate::parser::ast::Expr;
7use std::collections::HashMap;
8use std::sync::Arc;
9
10/// Aggregate operator.
11pub struct Aggregate {
12    /// GROUP BY expressions.
13    pub group_by: Vec<Expr>,
14    /// Aggregate functions.
15    pub aggregates: Vec<AggregateFunction>,
16}
17
18impl Aggregate {
19    /// Create a new aggregate operator.
20    pub fn new(group_by: Vec<Expr>, aggregates: Vec<AggregateFunction>) -> Self {
21        Self {
22            group_by,
23            aggregates,
24        }
25    }
26
27    /// Execute aggregation.
28    pub fn execute(&self, batch: &RecordBatch) -> Result<RecordBatch> {
29        if self.group_by.is_empty() {
30            // Global aggregation
31            self.execute_global_aggregate(batch)
32        } else {
33            // Grouped aggregation
34            self.execute_grouped_aggregate(batch)
35        }
36    }
37
38    /// Execute global aggregation (no GROUP BY).
39    fn execute_global_aggregate(&self, batch: &RecordBatch) -> Result<RecordBatch> {
40        let mut result_fields = Vec::new();
41        let mut result_columns = Vec::new();
42
43        for agg in &self.aggregates {
44            let value = if agg.column == "*" {
45                // COUNT(*) - count all rows regardless of NULL values
46                if matches!(agg.func, AggregateFunc::Count) {
47                    Some(batch.num_rows as f64)
48                } else {
49                    return Err(QueryError::semantic(
50                        "Wildcard (*) can only be used with COUNT function",
51                    ));
52                }
53            } else {
54                let column = batch
55                    .column_by_name(&agg.column)
56                    .ok_or_else(|| QueryError::ColumnNotFound(agg.column.clone()))?;
57                self.compute_aggregate(agg.func, column)?
58            };
59
60            result_fields.push(Field::new(
61                agg.alias.clone().unwrap_or_else(|| {
62                    if agg.column == "*" {
63                        "count".to_string()
64                    } else {
65                        agg.column.clone()
66                    }
67                }),
68                crate::executor::scan::DataType::Float64,
69                true,
70            ));
71            result_columns.push(ColumnData::Float64(vec![value]));
72        }
73
74        let schema = Arc::new(Schema::new(result_fields));
75        RecordBatch::new(schema, result_columns, 1)
76    }
77
78    /// Execute grouped aggregation (`GROUP BY`).
79    ///
80    /// Rows are partitioned into groups by evaluating every `GROUP BY`
81    /// expression per row and hashing the resulting scalar values (NULLs group
82    /// together, matching SQL semantics; composite keys are supported). For
83    /// each group the aggregate functions are computed over the member rows,
84    /// producing one output row per group. Output columns are the `GROUP BY`
85    /// keys (preserving their original types) followed by the aggregate results
86    /// (as `Float64`, matching the global-aggregation path).
87    fn execute_grouped_aggregate(&self, batch: &RecordBatch) -> Result<RecordBatch> {
88        let num_rows = batch.num_rows;
89
90        // Evaluate each GROUP BY expression for every row.
91        let mut group_values: Vec<Vec<Value>> = Vec::with_capacity(self.group_by.len());
92        for expr in &self.group_by {
93            let mut vals = Vec::with_capacity(num_rows);
94            for row in 0..num_rows {
95                let value = evaluate_expr_for_row(expr, batch, row)?;
96                if matches!(value, Value::Geometry(_)) {
97                    return Err(QueryError::unsupported(
98                        "GROUP BY on a geometry value is not supported",
99                    ));
100                }
101                vals.push(value);
102            }
103            group_values.push(vals);
104        }
105
106        // Partition rows into groups, preserving first-appearance order.
107        let mut key_to_group: HashMap<Vec<String>, usize> = HashMap::new();
108        let mut group_rows: Vec<Vec<usize>> = Vec::new();
109        let mut group_repr: Vec<Vec<Value>> = Vec::new();
110
111        for row in 0..num_rows {
112            let key: Vec<String> = group_values
113                .iter()
114                .map(|gv| group_key_component(&gv[row]))
115                .collect();
116            let gid = match key_to_group.get(&key) {
117                Some(&g) => g,
118                None => {
119                    let g = group_rows.len();
120                    key_to_group.insert(key, g);
121                    group_rows.push(Vec::new());
122                    group_repr.push(group_values.iter().map(|gv| gv[row].clone()).collect());
123                    g
124                }
125            };
126            group_rows[gid].push(row);
127        }
128
129        let num_groups = group_rows.len();
130
131        let mut result_fields = Vec::new();
132        let mut result_columns = Vec::new();
133
134        // GROUP BY key columns.
135        for (gi, expr) in self.group_by.iter().enumerate() {
136            let repr_vals: Vec<Value> =
137                (0..num_groups).map(|g| group_repr[g][gi].clone()).collect();
138            let column = values_to_column(&repr_vals);
139            let data_type = column_data_type(&column);
140            let name = match expr {
141                Expr::Column { name, .. } => name.clone(),
142                _ => format!("group_{}", gi),
143            };
144            result_fields.push(Field::new(name, data_type, true));
145            result_columns.push(column);
146        }
147
148        // Aggregate columns.
149        for agg in &self.aggregates {
150            let mut agg_vals: Vec<Option<f64>> = Vec::with_capacity(num_groups);
151            for rows in group_rows.iter() {
152                let value = if agg.column == "*" {
153                    if matches!(agg.func, AggregateFunc::Count) {
154                        Some(rows.len() as f64)
155                    } else {
156                        return Err(QueryError::semantic(
157                            "Wildcard (*) can only be used with COUNT function",
158                        ));
159                    }
160                } else {
161                    let column = batch
162                        .column_by_name(&agg.column)
163                        .ok_or_else(|| QueryError::ColumnNotFound(agg.column.clone()))?;
164                    let sub = gather_column(column, rows);
165                    self.compute_aggregate(agg.func, &sub)?
166                };
167                agg_vals.push(value);
168            }
169            result_fields.push(Field::new(
170                agg.alias.clone().unwrap_or_else(|| {
171                    if agg.column == "*" {
172                        "count".to_string()
173                    } else {
174                        agg.column.clone()
175                    }
176                }),
177                DataType::Float64,
178                true,
179            ));
180            result_columns.push(ColumnData::Float64(agg_vals));
181        }
182
183        let schema = Arc::new(Schema::new(result_fields));
184        RecordBatch::new(schema, result_columns, num_groups)
185    }
186
187    /// Compute aggregate function.
188    fn compute_aggregate(&self, func: AggregateFunc, column: &ColumnData) -> Result<Option<f64>> {
189        match func {
190            AggregateFunc::Count => Ok(Some(self.count(column))),
191            AggregateFunc::Sum => self.sum(column),
192            AggregateFunc::Avg => self.avg(column),
193            AggregateFunc::Min => self.min(column),
194            AggregateFunc::Max => self.max(column),
195        }
196    }
197
198    /// Count aggregate.
199    fn count(&self, column: &ColumnData) -> f64 {
200        let non_null_count = match column {
201            ColumnData::Boolean(data) => data.iter().filter(|v| v.is_some()).count(),
202            ColumnData::Int32(data) => data.iter().filter(|v| v.is_some()).count(),
203            ColumnData::Int64(data) => data.iter().filter(|v| v.is_some()).count(),
204            ColumnData::Float32(data) => data.iter().filter(|v| v.is_some()).count(),
205            ColumnData::Float64(data) => data.iter().filter(|v| v.is_some()).count(),
206            ColumnData::String(data) => data.iter().filter(|v| v.is_some()).count(),
207            ColumnData::Binary(data) => data.iter().filter(|v| v.is_some()).count(),
208        };
209        non_null_count as f64
210    }
211
212    /// Sum aggregate.
213    fn sum(&self, column: &ColumnData) -> Result<Option<f64>> {
214        match column {
215            ColumnData::Int32(data) => {
216                let sum: i64 = data.iter().filter_map(|v| v.map(|i| i as i64)).sum();
217                Ok(Some(sum as f64))
218            }
219            ColumnData::Int64(data) => {
220                let sum: i64 = data.iter().filter_map(|v| *v).sum();
221                Ok(Some(sum as f64))
222            }
223            ColumnData::Float32(data) => {
224                let sum: f32 = data.iter().filter_map(|v| *v).sum();
225                Ok(Some(sum as f64))
226            }
227            ColumnData::Float64(data) => {
228                let sum: f64 = data.iter().filter_map(|v| *v).sum();
229                Ok(Some(sum))
230            }
231            _ => Err(QueryError::type_mismatch("numeric", "non-numeric")),
232        }
233    }
234
235    /// Average aggregate.
236    fn avg(&self, column: &ColumnData) -> Result<Option<f64>> {
237        let sum = self.sum(column)?;
238        let count = self.count(column);
239        if count > 0.0 {
240            Ok(sum.map(|s| s / count))
241        } else {
242            Ok(None)
243        }
244    }
245
246    /// Minimum aggregate.
247    fn min(&self, column: &ColumnData) -> Result<Option<f64>> {
248        match column {
249            ColumnData::Int32(data) => {
250                let min = data.iter().filter_map(|v| *v).min();
251                Ok(min.map(|m| m as f64))
252            }
253            ColumnData::Int64(data) => {
254                let min = data.iter().filter_map(|v| *v).min();
255                Ok(min.map(|m| m as f64))
256            }
257            ColumnData::Float32(data) => {
258                let min = data
259                    .iter()
260                    .filter_map(|v| *v)
261                    .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
262                Ok(min.map(|m| m as f64))
263            }
264            ColumnData::Float64(data) => {
265                let min = data
266                    .iter()
267                    .filter_map(|v| *v)
268                    .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
269                Ok(min)
270            }
271            _ => Err(QueryError::type_mismatch("numeric", "non-numeric")),
272        }
273    }
274
275    /// Maximum aggregate.
276    fn max(&self, column: &ColumnData) -> Result<Option<f64>> {
277        match column {
278            ColumnData::Int32(data) => {
279                let max = data.iter().filter_map(|v| *v).max();
280                Ok(max.map(|m| m as f64))
281            }
282            ColumnData::Int64(data) => {
283                let max = data.iter().filter_map(|v| *v).max();
284                Ok(max.map(|m| m as f64))
285            }
286            ColumnData::Float32(data) => {
287                let max = data
288                    .iter()
289                    .filter_map(|v| *v)
290                    .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
291                Ok(max.map(|m| m as f64))
292            }
293            ColumnData::Float64(data) => {
294                let max = data
295                    .iter()
296                    .filter_map(|v| *v)
297                    .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
298                Ok(max)
299            }
300            _ => Err(QueryError::type_mismatch("numeric", "non-numeric")),
301        }
302    }
303}
304
305/// Build a hashable/equatable key component for a group-by value.
306///
307/// NULLs map to a single sentinel so they group together. Floats use their bit
308/// representation so that `NaN` values group deterministically.
309fn group_key_component(value: &Value) -> String {
310    match value {
311        Value::Null => "N".to_string(),
312        Value::Boolean(b) => format!("b{}", b),
313        Value::Int32(i) => format!("i{}", i),
314        Value::Int64(i) => format!("l{}", i),
315        Value::Float32(f) => format!("f{}", f.to_bits()),
316        Value::Float64(f) => format!("d{}", f.to_bits()),
317        Value::String(s) => format!("s{}", s),
318        // Geometry is rejected before this point.
319        Value::Geometry(_) => "g".to_string(),
320    }
321}
322
323/// Convert per-group representative [`Value`]s into a typed [`ColumnData`].
324///
325/// The column type is inferred from the first non-NULL value; an all-NULL group
326/// key defaults to an `Int64` column of NULLs.
327fn values_to_column(values: &[Value]) -> ColumnData {
328    let first_non_null = values.iter().find(|v| !matches!(v, Value::Null));
329    match first_non_null {
330        Some(Value::Boolean(_)) => ColumnData::Boolean(
331            values
332                .iter()
333                .map(|v| match v {
334                    Value::Boolean(b) => Some(*b),
335                    _ => None,
336                })
337                .collect(),
338        ),
339        Some(Value::Int32(_)) => ColumnData::Int32(
340            values
341                .iter()
342                .map(|v| match v {
343                    Value::Int32(i) => Some(*i),
344                    _ => None,
345                })
346                .collect(),
347        ),
348        Some(Value::Int64(_)) => ColumnData::Int64(
349            values
350                .iter()
351                .map(|v| match v {
352                    Value::Int64(i) => Some(*i),
353                    _ => None,
354                })
355                .collect(),
356        ),
357        Some(Value::Float32(_)) => ColumnData::Float32(
358            values
359                .iter()
360                .map(|v| match v {
361                    Value::Float32(f) => Some(*f),
362                    _ => None,
363                })
364                .collect(),
365        ),
366        Some(Value::Float64(_)) => ColumnData::Float64(
367            values
368                .iter()
369                .map(|v| match v {
370                    Value::Float64(f) => Some(*f),
371                    _ => None,
372                })
373                .collect(),
374        ),
375        Some(Value::String(_)) => ColumnData::String(
376            values
377                .iter()
378                .map(|v| match v {
379                    Value::String(s) => Some(s.clone()),
380                    _ => None,
381                })
382                .collect(),
383        ),
384        // All NULL (or geometry, which is rejected earlier): default to Int64 NULLs.
385        _ => ColumnData::Int64(values.iter().map(|_| None).collect()),
386    }
387}
388
389/// Map a [`ColumnData`] variant to its [`DataType`].
390fn column_data_type(column: &ColumnData) -> DataType {
391    match column {
392        ColumnData::Boolean(_) => DataType::Boolean,
393        ColumnData::Int32(_) => DataType::Int32,
394        ColumnData::Int64(_) => DataType::Int64,
395        ColumnData::Float32(_) => DataType::Float32,
396        ColumnData::Float64(_) => DataType::Float64,
397        ColumnData::String(_) => DataType::String,
398        ColumnData::Binary(_) => DataType::Binary,
399    }
400}
401
402/// Gather a subset of a column's rows (by index) into a new column.
403fn gather_column(column: &ColumnData, indices: &[usize]) -> ColumnData {
404    match column {
405        ColumnData::Boolean(d) => {
406            ColumnData::Boolean(indices.iter().filter_map(|&i| d.get(i).copied()).collect())
407        }
408        ColumnData::Int32(d) => {
409            ColumnData::Int32(indices.iter().filter_map(|&i| d.get(i).copied()).collect())
410        }
411        ColumnData::Int64(d) => {
412            ColumnData::Int64(indices.iter().filter_map(|&i| d.get(i).copied()).collect())
413        }
414        ColumnData::Float32(d) => {
415            ColumnData::Float32(indices.iter().filter_map(|&i| d.get(i).copied()).collect())
416        }
417        ColumnData::Float64(d) => {
418            ColumnData::Float64(indices.iter().filter_map(|&i| d.get(i).copied()).collect())
419        }
420        ColumnData::String(d) => {
421            ColumnData::String(indices.iter().filter_map(|&i| d.get(i).cloned()).collect())
422        }
423        ColumnData::Binary(d) => {
424            ColumnData::Binary(indices.iter().filter_map(|&i| d.get(i).cloned()).collect())
425        }
426    }
427}
428
429/// Aggregate function.
430#[derive(Debug, Clone)]
431pub struct AggregateFunction {
432    /// Function type.
433    pub func: AggregateFunc,
434    /// Column to aggregate.
435    pub column: String,
436    /// Output alias.
437    pub alias: Option<String>,
438}
439
440/// Aggregate function type.
441#[derive(Debug, Clone, Copy, PartialEq, Eq)]
442pub enum AggregateFunc {
443    /// COUNT function.
444    Count,
445    /// SUM function.
446    Sum,
447    /// AVG function.
448    Avg,
449    /// MIN function.
450    Min,
451    /// MAX function.
452    Max,
453}
454
455#[cfg(test)]
456#[allow(clippy::panic)]
457mod tests {
458    use super::*;
459    use crate::executor::scan::DataType;
460
461    #[test]
462    fn test_global_aggregate() -> Result<()> {
463        let schema = Arc::new(Schema::new(vec![Field::new(
464            "value".to_string(),
465            DataType::Int64,
466            false,
467        )]));
468
469        let columns = vec![ColumnData::Int64(vec![
470            Some(10),
471            Some(20),
472            Some(30),
473            Some(40),
474            Some(50),
475        ])];
476
477        let batch = RecordBatch::new(schema, columns, 5)?;
478
479        let agg = Aggregate::new(
480            vec![],
481            vec![
482                AggregateFunction {
483                    func: AggregateFunc::Sum,
484                    column: "value".to_string(),
485                    alias: Some("sum".to_string()),
486                },
487                AggregateFunction {
488                    func: AggregateFunc::Avg,
489                    column: "value".to_string(),
490                    alias: Some("avg".to_string()),
491                },
492            ],
493        );
494
495        let result = agg.execute(&batch)?;
496        assert_eq!(result.num_rows, 1);
497        assert_eq!(result.columns.len(), 2);
498
499        Ok(())
500    }
501
502    #[test]
503    fn test_grouped_aggregate() -> Result<()> {
504        let schema = Arc::new(Schema::new(vec![
505            Field::new("k".to_string(), DataType::Int64, false),
506            Field::new("v".to_string(), DataType::Int64, false),
507        ]));
508        // groups: k=1 -> [10, 30] (count 2, sum 40); k=2 -> [20] (count 1, sum 20)
509        let columns = vec![
510            ColumnData::Int64(vec![Some(1), Some(2), Some(1)]),
511            ColumnData::Int64(vec![Some(10), Some(20), Some(30)]),
512        ];
513        let batch = RecordBatch::new(schema, columns, 3)?;
514
515        let agg = Aggregate::new(
516            vec![Expr::Column {
517                table: None,
518                name: "k".to_string(),
519            }],
520            vec![
521                AggregateFunction {
522                    func: AggregateFunc::Sum,
523                    column: "v".to_string(),
524                    alias: Some("sum_v".to_string()),
525                },
526                AggregateFunction {
527                    func: AggregateFunc::Count,
528                    column: "*".to_string(),
529                    alias: Some("cnt".to_string()),
530                },
531            ],
532        );
533
534        let result = agg.execute(&batch)?;
535        assert_eq!(result.num_rows, 2); // two groups
536        assert_eq!(result.columns.len(), 3); // k, sum_v, cnt
537
538        // Group key column preserves the Int64 type and first-appearance order.
539        let ColumnData::Int64(k) = &result.columns[0] else {
540            panic!("expected int64 group key");
541        };
542        assert_eq!(k[0], Some(1));
543        assert_eq!(k[1], Some(2));
544
545        let ColumnData::Float64(sum_v) = &result.columns[1] else {
546            panic!("expected float64 sum");
547        };
548        assert_eq!(sum_v[0], Some(40.0));
549        assert_eq!(sum_v[1], Some(20.0));
550
551        let ColumnData::Float64(cnt) = &result.columns[2] else {
552            panic!("expected float64 count");
553        };
554        assert_eq!(cnt[0], Some(2.0));
555        assert_eq!(cnt[1], Some(1.0));
556
557        Ok(())
558    }
559
560    #[test]
561    fn test_grouped_aggregate_null_key_groups_together() -> Result<()> {
562        let schema = Arc::new(Schema::new(vec![
563            Field::new("k".to_string(), DataType::Int64, true),
564            Field::new("v".to_string(), DataType::Int64, false),
565        ]));
566        let columns = vec![
567            ColumnData::Int64(vec![None, Some(1), None]),
568            ColumnData::Int64(vec![Some(5), Some(7), Some(9)]),
569        ];
570        let batch = RecordBatch::new(schema, columns, 3)?;
571        let agg = Aggregate::new(
572            vec![Expr::Column {
573                table: None,
574                name: "k".to_string(),
575            }],
576            vec![AggregateFunction {
577                func: AggregateFunc::Count,
578                column: "*".to_string(),
579                alias: Some("cnt".to_string()),
580            }],
581        );
582        let result = agg.execute(&batch)?;
583        // NULL group (2 rows) + k=1 group (1 row) => 2 groups.
584        assert_eq!(result.num_rows, 2);
585        let ColumnData::Float64(cnt) = &result.columns[1] else {
586            panic!("expected float64 count");
587        };
588        assert_eq!(cnt[0], Some(2.0)); // NULL group first (rows 0 and 2)
589        assert_eq!(cnt[1], Some(1.0));
590        Ok(())
591    }
592}