Skip to main content

akar_processor/physical/order_aggregate/
aggregatehashtable.rs

1//! Auto-extracted from physical_operator.rs
2use crate::physical::common::{hash_value_into, store_value_in_vector};
3use crate::physical::types::OperatorResult;
4use akar_common::types::{PhysicalTypeID, Value};
5use akar_common::vector::{DataChunk, ValueVector};
6use akar_function::AggregateFunction;
7use akar_function::aggregate::AggValueState;
8use akar_parser::ast::Expression;
9use arrow::compute;
10use arrow::datatypes::{Float64Type, Int32Type, Int64Type};
11
12// ==================== AggregateHashTable ====================
13
14/// A parallel hash table for aggregation with GROUP BY keys.
15///
16/// Uses `rayon` thread-local aggregation: each thread builds its own
17/// local hash table, then all local tables are merged.
18pub struct AggregateHashTable {
19    /// Pre-parsed aggregate functions.
20    funcs: Vec<AggregateFunction>,
21    /// Group-by key column indices.
22    group_by_cols: Vec<u32>,
23    /// Aggregate function argument expressions (for resolving column indices).
24    agg_expressions: Vec<Vec<Expression>>,
25}
26
27impl AggregateHashTable {
28    pub fn new(funcs: Vec<AggregateFunction>, group_by_cols: Vec<u32>, agg_expressions: Vec<Vec<Expression>>) -> Self {
29        Self {
30            funcs,
31            group_by_cols,
32            agg_expressions,
33        }
34    }
35
36    /// Aggregate all input chunks, optionally in parallel.
37    pub fn aggregate(&self, chunks: &[DataChunk]) -> OperatorResult {
38        let total_rows: usize = chunks.iter().map(|c| c.size).sum();
39
40        // Resolve aggregate expression args to column indices
41        let mut col_indices = resolve_agg_col_indices(
42            &self.agg_expressions,
43            chunks.first().map(|c| c.field_names.as_slice()).unwrap_or(&[]),
44        );
45
46        // PhysicalAggregate (simple path) carries no agg expressions, so each
47        // function operates on the first input column. Guard against the empty
48        // vec to keep the fast paths below from indexing out of bounds.
49        if col_indices.is_empty() && !self.funcs.is_empty() {
50            col_indices = vec![Some(0); self.funcs.len()];
51        }
52
53        if total_rows == 0 && self.group_by_cols.is_empty() {
54            // Scalar aggregates on empty input: produce one row with default values
55            // (COUNT=0, SUM=Null, etc.)
56            let mut fields = Vec::new();
57            for func in &self.funcs {
58                let state = AggValueState::new(func);
59                let result = state.finalize();
60                let phys_type = result.physical_type();
61                let mut v = ValueVector::new(phys_type, 1);
62                v.resize(1);
63                store_value_in_vector(&mut v, 0, &result)?;
64                fields.push(v);
65            }
66            return Ok(vec![{
67                let arrow_fields = fields
68                    .iter()
69                    .map(|v| akar_common::arrow_vector::ArrowVector::from_legacy(v).array)
70                    .collect::<Vec<_>>();
71                let arrow_field_types = fields.iter().map(|v| v.physical_type()).collect::<Vec<_>>();
72                DataChunk::new(arrow_fields, arrow_field_types)
73            }]);
74        }
75
76        if total_rows == 0 {
77            return self.empty_result();
78        }
79
80        // Fast path for scalar COUNT aggregates (no GROUP BY)
81        if self.group_by_cols.is_empty() {
82            let all_count = self
83                .funcs
84                .iter()
85                .all(|f| matches!(f, AggregateFunction::Count | AggregateFunction::CountStar));
86            if all_count {
87                let mut fields = Vec::new();
88                for (i, func) in self.funcs.iter().enumerate() {
89                    let mut total = 0u64;
90                    for chunk in chunks {
91                        match func {
92                            AggregateFunction::CountStar => {
93                                total += chunk.active_rows() as u64;
94                            }
95                            AggregateFunction::Count => {
96                                if let Some(col_idx) = col_indices[i] {
97                                    if let Some(field) = chunk.fields.get(col_idx) {
98                                        if chunk.sel_vector.is_some() {
99                                            for row in chunk.iter_rows() {
100                                                if !field.is_null(row) {
101                                                    total += 1;
102                                                }
103                                            }
104                                        } else {
105                                            total += (field.len() - field.null_count()) as u64;
106                                        }
107                                    }
108                                } else {
109                                    total += chunk.active_rows() as u64;
110                                }
111                            }
112                            _ => {}
113                        }
114                    }
115                    let state = AggValueState::Count(total);
116                    let result = state.finalize();
117                    let phys_type = result.physical_type();
118                    let mut v = ValueVector::new(phys_type, 1);
119                    v.resize(1);
120                    store_value_in_vector(&mut v, 0, &result)?;
121                    fields.push(v);
122                }
123                return Ok(vec![{
124                    let arrow_fields = fields
125                        .iter()
126                        .map(|v| akar_common::arrow_vector::ArrowVector::from_legacy(v).array)
127                        .collect::<Vec<_>>();
128                    let arrow_field_types = fields.iter().map(|v| v.physical_type()).collect::<Vec<_>>();
129                    DataChunk::new(arrow_fields, arrow_field_types)
130                }]);
131            }
132        }
133
134        // Fast path for scalar Sum/Min/Max/Avg aggregates (no GROUP BY).
135        // Uses Arrow compute kernels directly on ArrayRef — avoids per-row Value dispatch.
136        if self.group_by_cols.is_empty() && chunks.iter().all(|c| c.sel_vector.is_none()) {
137            let all_scalar_agg = self.funcs.iter().all(|f| {
138                matches!(
139                    f,
140                    AggregateFunction::Sum | AggregateFunction::Min | AggregateFunction::Max | AggregateFunction::Avg
141                )
142            });
143            if all_scalar_agg {
144                let mut fields = Vec::new();
145                for (i, func) in self.funcs.iter().enumerate() {
146                    if let Some(col_idx) = col_indices[i] {
147                        let result_val = arrow_scalar_agg(func, chunks, col_idx);
148                        let phys_type = result_val.physical_type();
149                        let mut v = ValueVector::new(phys_type, 1);
150                        v.resize(1);
151                        store_value_in_vector(&mut v, 0, &result_val)?;
152                        fields.push(v);
153                    } else {
154                        let mut v = ValueVector::new(PhysicalTypeID::Int64, 1);
155                        v.resize(1);
156                        v.set_null(0, true);
157                        fields.push(v);
158                    }
159                }
160                return Ok(vec![DataChunk::new(
161                    fields
162                        .iter()
163                        .map(|v| akar_common::arrow_vector::ArrowVector::from_legacy(v).array)
164                        .collect(),
165                    fields.iter().map(|v| v.physical_type()).collect(),
166                )]);
167            }
168        }
169
170        // Use rayon parallel aggregation for large inputs
171        if total_rows > 1000 {
172            self.aggregate_parallel(chunks, &col_indices)
173        } else {
174            self.aggregate_sequential(chunks, &col_indices)
175        }
176    }
177
178    /// Parallel aggregation: split chunks across threads, aggregate locally, merge.
179    fn aggregate_parallel(&self, chunks: &[DataChunk], col_indices: &[Option<usize>]) -> OperatorResult {
180        use rayon::prelude::*;
181        let total_rows: usize = chunks.iter().map(|c| c.size).sum();
182        type LocalTable = hashbrown::HashMap<u64, Vec<(Value, Vec<AggValueState>)>>;
183
184        // Each thread aggregates its portion
185        let group_cols = &self.group_by_cols;
186        let funcs = &self.funcs;
187
188        let results: Vec<LocalTable> = chunks
189            .par_iter()
190            .map(|chunk| {
191                let mut local: LocalTable = hashbrown::HashMap::with_capacity(chunk.size.max(16));
192                for row in chunk.iter_rows() {
193                    let hash = hash_group_key(chunk, group_cols, row);
194                    let bucket = local.entry(hash).or_default();
195                    let entry = bucket.iter_mut().find(|(k, _)| keys_equal(k, chunk, group_cols, row));
196                    if let Some((_, states)) = entry {
197                        update_states_row(states, chunk, funcs, col_indices, row);
198                    } else {
199                        let key = build_group_key(chunk, group_cols, row);
200                        let mut states = funcs.iter().map(AggValueState::new).collect::<Vec<_>>();
201                        update_states_row(&mut states, chunk, funcs, col_indices, row);
202                        bucket.push((key, states));
203                    }
204                }
205                local
206            })
207            .collect();
208
209        // Merge all local tables
210        let mut merged: LocalTable = hashbrown::HashMap::with_capacity(total_rows.max(16));
211        for local in results {
212            for (hash, bucket) in local {
213                let mbucket = merged.entry(hash).or_default();
214                for (key, states) in bucket {
215                    let entry = mbucket.iter_mut().find(|(k, _)| *k == key);
216                    if let Some((_, existing)) = entry {
217                        for (i, s) in states.iter().enumerate() {
218                            existing[i].merge(s);
219                        }
220                    } else {
221                        mbucket.push((key, states));
222                    }
223                }
224            }
225        }
226        self.build_output(&merged)
227    }
228
229    /// Sequential aggregation (small inputs).
230    fn aggregate_sequential(&self, chunks: &[DataChunk], col_indices: &[Option<usize>]) -> OperatorResult {
231        let total_rows: usize = chunks.iter().map(|c| c.size).sum();
232        let mut groups: hashbrown::HashMap<u64, Vec<(Value, Vec<AggValueState>)>> =
233            hashbrown::HashMap::with_capacity(total_rows.max(16));
234        let group_cols = &self.group_by_cols;
235        let funcs = &self.funcs;
236
237        for chunk in chunks {
238            for row in chunk.iter_rows() {
239                let hash = hash_group_key(chunk, group_cols, row);
240                let bucket = groups.entry(hash).or_default();
241                let entry = bucket.iter_mut().find(|(k, _)| keys_equal(k, chunk, group_cols, row));
242                if let Some((_, states)) = entry {
243                    update_states_row(states, chunk, funcs, col_indices, row);
244                } else {
245                    let key = build_group_key(chunk, group_cols, row);
246                    let mut states = funcs.iter().map(AggValueState::new).collect::<Vec<_>>();
247                    update_states_row(&mut states, chunk, funcs, col_indices, row);
248                    bucket.push((key, states));
249                }
250            }
251        }
252        self.build_output(&groups)
253    }
254
255    fn empty_result(&self) -> OperatorResult {
256        let num_cols = self.group_by_cols.len() + self.funcs.len();
257        Ok(vec![DataChunk::new(
258            Vec::with_capacity(num_cols),
259            Vec::with_capacity(num_cols),
260        )])
261    }
262
263    pub fn build_output(&self, groups: &hashbrown::HashMap<u64, Vec<(Value, Vec<AggValueState>)>>) -> OperatorResult {
264        if groups.is_empty() {
265            return self.empty_result();
266        }
267
268        // Flatten buckets into parallel arrays
269        let mut group_keys: Vec<Value> = Vec::new();
270        let mut agg_results: Vec<Vec<Value>> = (0..self.funcs.len()).map(|_| Vec::new()).collect();
271
272        for bucket in groups.values() {
273            for (key, states) in bucket {
274                group_keys.push(key.clone());
275                for (i, state) in states.iter().enumerate() {
276                    agg_results[i].push(state.finalize());
277                }
278            }
279        }
280
281        let num_group_cols = self.group_by_cols.len();
282        let num_rows = group_keys.len();
283        let mut output = Vec::with_capacity(num_group_cols + self.funcs.len());
284
285        // Group key columns
286        if num_group_cols == 1 {
287            let first_val = &group_keys[0];
288            let phys_type = first_val.physical_type();
289            let mut v = ValueVector::new(phys_type, num_rows);
290            v.resize(num_rows);
291            for (row, key) in group_keys.iter().enumerate() {
292                if matches!(key, Value::Null) {
293                    v.set_null(row, true);
294                } else {
295                    store_value_in_vector(&mut v, row, key)?;
296                }
297            }
298            output.push(v);
299        } else {
300            for gc_idx in 0..num_group_cols {
301                let first_key = &group_keys[0];
302                let inner_val = match first_key {
303                    Value::List(vals) => vals.get(gc_idx).cloned().unwrap_or(Value::Null),
304                    _ => Value::Null,
305                };
306                let phys_type = inner_val.physical_type();
307                let mut v = ValueVector::new(phys_type, num_rows);
308                v.resize(num_rows);
309                for (row, key) in group_keys.iter().enumerate() {
310                    let val = match key {
311                        Value::List(vals) => vals.get(gc_idx).cloned().unwrap_or(Value::Null),
312                        _ => Value::Null,
313                    };
314                    if matches!(val, Value::Null) {
315                        v.set_null(row, true);
316                    } else {
317                        store_value_in_vector(&mut v, row, &val)?;
318                    }
319                }
320                output.push(v);
321            }
322        }
323
324        // Aggregate result columns
325        for i in 0..self.funcs.len() {
326            let first_val = &agg_results[i][0];
327            let phys_type = first_val.physical_type();
328            let mut v = ValueVector::new(phys_type, num_rows);
329            v.resize(num_rows);
330            for (row, val) in agg_results[i].iter().enumerate() {
331                store_value_in_vector(&mut v, row, val)?;
332            }
333            output.push(v);
334        }
335
336        // Split output into chunks of 2048
337        const CHUNK_SIZE: usize = 2048;
338        let mut chunks = Vec::new();
339
340        for chunk_start in (0..num_rows).step_by(CHUNK_SIZE) {
341            let chunk_end = (chunk_start + CHUNK_SIZE).min(num_rows);
342            let chunk_len = chunk_end - chunk_start;
343
344            let mut chunk_fields = Vec::with_capacity(output.len());
345            for field in &output {
346                let mut new_v = ValueVector::new(field.physical_type(), chunk_len);
347                new_v.resize(chunk_len);
348                for i in 0..chunk_len {
349                    if field.is_null(chunk_start + i) {
350                        new_v.set_null(i, true);
351                    } else if let Some(val) = field.get_value(chunk_start + i) {
352                        store_value_in_vector(&mut new_v, i, &val)?;
353                    }
354                }
355                chunk_fields.push(new_v);
356            }
357            chunks.push({
358                let arrow_fields = chunk_fields
359                    .iter()
360                    .map(|v| akar_common::arrow_vector::ArrowVector::from_legacy(v).array)
361                    .collect::<Vec<_>>();
362                let arrow_field_types = chunk_fields.iter().map(|v| v.physical_type()).collect::<Vec<_>>();
363                DataChunk::new(arrow_fields, arrow_field_types)
364            });
365        }
366
367        Ok(chunks)
368    }
369}
370
371/// Hash group key columns directly from Arrow arrays without creating intermediate `Value` objects.
372/// For strings, avoids the `to_string()` allocation that `get_value()` would incur.
373/// For primitives, avoids `Value` enum dispatch overhead.
374pub fn hash_group_key(chunk: &DataChunk, group_cols: &[u32], row: usize) -> u64 {
375    use std::hash::Hash;
376    use std::hash::Hasher;
377    let mut hasher = ahash::AHasher::default();
378    for &gc in group_cols {
379        let col = gc as usize;
380        if col >= chunk.fields.len() {
381            0u8.hash(&mut hasher);
382            continue;
383        }
384        if chunk.is_null(col, row) {
385            0u8.hash(&mut hasher);
386            continue;
387        }
388        match chunk.field_types[col] {
389            PhysicalTypeID::Int64 => {
390                let v = chunk.get_i64(col, row).unwrap_or(0);
391                v.hash(&mut hasher);
392            }
393            PhysicalTypeID::Int32 => {
394                let v = chunk.get_i32(col, row).unwrap_or(0);
395                v.hash(&mut hasher);
396            }
397            PhysicalTypeID::Double => {
398                let v = chunk.get_f64(col, row).unwrap_or(0.0);
399                v.to_bits().hash(&mut hasher);
400            }
401            PhysicalTypeID::Bool => {
402                let v = chunk.get_bool(col, row).unwrap_or(false);
403                v.hash(&mut hasher);
404            }
405            PhysicalTypeID::String => {
406                if let Some(s) = chunk.get_string(col, row) {
407                    s.hash(&mut hasher);
408                }
409            }
410            _ => {
411                // Fallback: create Value for types we don't handle directly
412                if let Some(val) = chunk.get_value(col, row) {
413                    hash_value_into(&val, &mut hasher);
414                }
415            }
416        }
417    }
418    hasher.finish()
419}
420
421/// Check if a stored group key matches the current row's group column values.
422/// Avoids creating Value::List or intermediate Value objects for comparison.
423pub fn keys_equal(stored: &Value, chunk: &DataChunk, group_cols: &[u32], row: usize) -> bool {
424    if group_cols.len() == 1 {
425        let col = group_cols[0] as usize;
426        if col >= chunk.fields.len() {
427            return *stored == Value::Null;
428        }
429        if chunk.is_null(col, row) {
430            return *stored == Value::Null;
431        }
432        return match stored {
433            Value::Int64(v) => chunk.get_i64(col, row).is_some_and(|x| x == *v),
434            Value::Int32(v) => chunk.get_i32(col, row).is_some_and(|x| x == *v),
435            Value::Double(v) => chunk.get_f64(col, row).is_some_and(|x| x == *v),
436            Value::Bool(v) => chunk.get_bool(col, row).is_some_and(|x| x == *v),
437            Value::String(v) => chunk.get_string(col, row).is_some_and(|x| x == *v),
438            _ => {
439                let val = chunk.get_value(col, row).unwrap_or(Value::Null);
440                *stored == val
441            }
442        };
443    }
444    match stored {
445        Value::List(vals) if vals.len() == group_cols.len() => vals.iter().enumerate().all(|(i, v)| {
446            let col = group_cols[i] as usize;
447            if col >= chunk.fields.len() {
448                return *v == Value::Null;
449            }
450            if chunk.is_null(col, row) {
451                return *v == Value::Null;
452            }
453            match v {
454                Value::Int64(expected) => chunk.get_i64(col, row).is_some_and(|x| x == *expected),
455                Value::Int32(expected) => chunk.get_i32(col, row).is_some_and(|x| x == *expected),
456                Value::Double(expected) => chunk.get_f64(col, row).is_some_and(|x| x == *expected),
457                Value::Bool(expected) => chunk.get_bool(col, row).is_some_and(|x| x == *expected),
458                Value::String(expected) => chunk.get_string(col, row).is_some_and(|x| x == *expected),
459                _ => {
460                    let val = chunk.get_value(col, row).unwrap_or(Value::Null);
461                    *v == val
462                }
463            }
464        }),
465        _ => false,
466    }
467}
468
469/// Build a composite group key from chunk columns.
470pub fn build_group_key(chunk: &DataChunk, group_cols: &[u32], row: usize) -> Value {
471    if group_cols.is_empty() {
472        return Value::Null;
473    }
474    if group_cols.len() == 1 {
475        chunk
476            .fields
477            .get(group_cols[0] as usize)
478            .map(|_| chunk.get_value(group_cols[0] as usize, row))
479            .unwrap_or(Some(Value::Null))
480            .unwrap_or(Value::Null)
481    } else {
482        let vals: Vec<Value> = group_cols
483            .iter()
484            .map(|&gc| {
485                chunk
486                    .fields
487                    .get(gc as usize)
488                    .map(|_| chunk.get_value(gc as usize, row))
489                    .unwrap_or(Some(Value::Null))
490                    .unwrap_or(Value::Null)
491            })
492            .collect();
493        Value::List(vals)
494    }
495}
496
497/// Resolve an expression name to a column index using flexible matching.
498/// Tries: exact match, numeric parse, ends_with(".name").
499fn resolve_name_to_col(name: &str, field_names: &[String]) -> Option<usize> {
500    if let Some(idx) = field_names.iter().position(|n| n == name) {
501        return Some(idx);
502    }
503    if let Ok(idx) = name.parse::<usize>() {
504        if idx < field_names.len() {
505            return Some(idx);
506        }
507    }
508    let dot_name = format!(".{}", name);
509    if let Some(idx) = field_names.iter().position(|n| n.ends_with(&dot_name)) {
510        return Some(idx);
511    }
512    None
513}
514
515/// Resolve aggregate function argument expressions to column indices.
516/// Returns one Option per function: None means no column needed (e.g., COUNT(*)).
517pub fn resolve_agg_col_indices(agg_expressions: &[Vec<Expression>], field_names: &[String]) -> Vec<Option<usize>> {
518    agg_expressions
519        .iter()
520        .map(|args| {
521            for expr in args {
522                match expr {
523                    Expression::Variable(name) => {
524                        if let Some(idx) = resolve_name_to_col(name, field_names) {
525                            return Some(idx);
526                        }
527                    }
528                    Expression::PropertyAccess(base, prop) => {
529                        if let Expression::Variable(prefix) = base.as_ref() {
530                            // Try "prefix.prop" (exact match with Cypher variable name)
531                            let qualified = format!("{}.{}", prefix, prop);
532                            if let Some(idx) = field_names.iter().position(|n| n == &qualified) {
533                                return Some(idx);
534                            }
535                            // Try ".prop" suffix match (handles table-prefixed names like "Person.age")
536                            let dot_prop = format!(".{}", prop);
537                            if let Some(idx) = field_names.iter().position(|n| n.ends_with(&dot_prop)) {
538                                return Some(idx);
539                            }
540                        }
541                    }
542                    Expression::Star => return None,
543                    _ => {}
544                }
545            }
546            None
547        })
548        .collect()
549}
550
551/// Resolve GROUP BY expressions to actual column indices using field_names.
552pub fn resolve_group_by_indices(group_by: &[Expression], field_names: &[String]) -> Vec<u32> {
553    group_by
554        .iter()
555        .map(|expr| match expr {
556            Expression::Variable(name) => resolve_name_to_col(name, field_names).unwrap_or(0) as u32,
557            Expression::PropertyAccess(base, prop) => {
558                if let Expression::Variable(prefix) = base.as_ref() {
559                    let qualified = format!("{}.{}", prefix, prop);
560                    if let Some(idx) = field_names.iter().position(|n| n == &qualified) {
561                        return idx as u32;
562                    }
563                    let dot_prop = format!(".{}", prop);
564                    if let Some(idx) = field_names.iter().position(|n| n.ends_with(&dot_prop)) {
565                        return idx as u32;
566                    }
567                }
568                0
569            }
570            _ => 0,
571        })
572        .collect()
573}
574
575/// Update aggregate states for a single row.
576pub fn update_states_row(
577    states: &mut [AggValueState],
578    chunk: &DataChunk,
579    funcs: &[AggregateFunction],
580    col_indices: &[Option<usize>],
581    row: usize,
582) {
583    for (i, state) in states.iter_mut().enumerate() {
584        if matches!(funcs[i], AggregateFunction::CountStar) {
585            if let AggValueState::Count(n) = state {
586                *n += 1;
587            }
588            continue;
589        }
590        if col_indices.get(i).copied().flatten().is_none() && matches!(funcs[i], AggregateFunction::Count) {
591            if let AggValueState::Count(n) = state {
592                *n += 1;
593            }
594            continue;
595        }
596        let col_idx = col_indices
597            .get(i)
598            .copied()
599            .flatten()
600            .unwrap_or_else(|| i.min(chunk.fields.len().saturating_sub(1)));
601        let val = chunk
602            .fields
603            .get(col_idx)
604            .map(|_| chunk.get_value(col_idx, row))
605            .unwrap_or(Some(Value::Null))
606            .unwrap_or(Value::Null);
607        state.update(&val);
608    }
609}
610
611/// Compute a scalar aggregate (Sum/Min/Max/Avg) over chunks using Arrow compute kernels.
612/// Avoids per-row Value dispatch entirely.
613fn arrow_scalar_agg(func: &AggregateFunction, chunks: &[DataChunk], col_idx: usize) -> Value {
614    // Collect non-null numeric values from all chunks into a single Arrow array
615    let all_values: Vec<Value> = chunks
616        .iter()
617        .flat_map(|c| {
618            if col_idx >= c.fields.len() {
619                return Vec::new();
620            }
621            let field = &c.fields[col_idx];
622            let rows = if c.sel_vector.is_some() {
623                c.iter_rows().collect::<Vec<_>>()
624            } else {
625                (0..c.size).collect::<Vec<_>>()
626            };
627            rows.into_iter()
628                .filter_map(|row| {
629                    if field.is_null(row) {
630                        None
631                    } else {
632                        c.get_value(col_idx, row)
633                    }
634                })
635                .collect::<Vec<_>>()
636        })
637        .collect();
638
639    if all_values.is_empty() {
640        return match func {
641            AggregateFunction::Sum | AggregateFunction::Avg => Value::Null,
642            AggregateFunction::Min | AggregateFunction::Max => Value::Null,
643            _ => Value::Null,
644        };
645    }
646
647    // Try to use Arrow compute kernels on primitive arrays
648    if let Some(arr) = values_to_prim_array(&all_values) {
649        return match func {
650            AggregateFunction::Sum => compute_sum(&arr),
651            AggregateFunction::Min => compute_min(&arr),
652            AggregateFunction::Max => compute_max(&arr),
653            AggregateFunction::Avg => {
654                let sum_val = compute_sum_f64(&arr);
655                let count = non_null_count(chunks, col_idx);
656                if count == 0 {
657                    Value::Null
658                } else {
659                    Value::Double(sum_val / count as f64)
660                }
661            }
662            _ => Value::Null,
663        };
664    }
665
666    // Fallback: per-row Value dispatch (for non-numeric types)
667    let mut state = AggValueState::new(func);
668    for val in &all_values {
669        state.update(val);
670    }
671    state.finalize()
672}
673
674enum PrimArray {
675    I64(arrow::array::PrimitiveArray<Int64Type>),
676    I32(arrow::array::PrimitiveArray<Int32Type>),
677    F64(arrow::array::PrimitiveArray<Float64Type>),
678}
679
680fn values_to_prim_array(vals: &[Value]) -> Option<PrimArray> {
681    match &vals[0] {
682        Value::Int64(_) => {
683            let mut b = arrow::array::Int64Builder::with_capacity(vals.len());
684            for v in vals {
685                match v {
686                    Value::Int64(n) => b.append_value(*n),
687                    Value::Null => b.append_null(),
688                    _ => return None,
689                }
690            }
691            Some(PrimArray::I64(b.finish()))
692        }
693        Value::Int32(_) => {
694            let mut b = arrow::array::Int32Builder::with_capacity(vals.len());
695            for v in vals {
696                match v {
697                    Value::Int32(n) => b.append_value(*n),
698                    Value::Null => b.append_null(),
699                    _ => return None,
700                }
701            }
702            Some(PrimArray::I32(b.finish()))
703        }
704        Value::Double(_) => {
705            let mut b = arrow::array::Float64Builder::with_capacity(vals.len());
706            for v in vals {
707                match v {
708                    Value::Double(n) => b.append_value(*n),
709                    Value::Null => b.append_null(),
710                    _ => return None,
711                }
712            }
713            Some(PrimArray::F64(b.finish()))
714        }
715        _ => None,
716    }
717}
718
719fn compute_sum(arr: &PrimArray) -> Value {
720    match arr {
721        PrimArray::I64(a) => compute::sum(a).map(Value::Int64).unwrap_or(Value::Null),
722        PrimArray::I32(a) => compute::sum(a).map(|v| Value::Int64(v as i64)).unwrap_or(Value::Null),
723        PrimArray::F64(a) => compute::sum(a).map(Value::Double).unwrap_or(Value::Null),
724    }
725}
726
727fn compute_sum_f64(arr: &PrimArray) -> f64 {
728    match arr {
729        PrimArray::I64(a) => compute::sum::<Int64Type>(a).unwrap_or(0) as f64,
730        PrimArray::I32(a) => compute::sum::<Int32Type>(a).unwrap_or(0) as f64,
731        PrimArray::F64(a) => compute::sum::<Float64Type>(a).unwrap_or(0.0),
732    }
733}
734
735fn compute_min(arr: &PrimArray) -> Value {
736    match arr {
737        PrimArray::I64(a) => compute::min(a).map(Value::Int64).unwrap_or(Value::Null),
738        PrimArray::I32(a) => compute::min(a).map(|v| Value::Int64(v as i64)).unwrap_or(Value::Null),
739        PrimArray::F64(a) => compute::min(a).map(Value::Double).unwrap_or(Value::Null),
740    }
741}
742
743fn compute_max(arr: &PrimArray) -> Value {
744    match arr {
745        PrimArray::I64(a) => compute::max(a).map(Value::Int64).unwrap_or(Value::Null),
746        PrimArray::I32(a) => compute::max(a).map(|v| Value::Int64(v as i64)).unwrap_or(Value::Null),
747        PrimArray::F64(a) => compute::max(a).map(Value::Double).unwrap_or(Value::Null),
748    }
749}
750
751fn non_null_count(chunks: &[DataChunk], col_idx: usize) -> u64 {
752    let mut count = 0u64;
753    for c in chunks {
754        if col_idx >= c.fields.len() {
755            continue;
756        }
757        let field = &c.fields[col_idx];
758        if c.sel_vector.is_some() {
759            for row in c.iter_rows() {
760                if !field.is_null(row) {
761                    count += 1;
762                }
763            }
764        } else {
765            count += (field.len() - field.null_count()) as u64;
766        }
767    }
768    count
769}