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    /// Scalar aggregate (no GROUP BY) over empty input: emit one row where each
264    /// aggregate column holds its initial state's final value (COUNT=0, SUM=NULL,
265    /// MIN/MAX/AVG=NULL, COLLECT=[], ...).
266    fn empty_scalar_result(&self) -> OperatorResult {
267        let mut fields = Vec::new();
268        for func in &self.funcs {
269            let state = AggValueState::new(func);
270            let result = state.finalize();
271            let phys_type = result.physical_type();
272            let mut v = ValueVector::new(phys_type, 1);
273            v.resize(1);
274            store_value_in_vector(&mut v, 0, &result)?;
275            fields.push(v);
276        }
277        Ok(vec![{
278            let arrow_fields = fields
279                .iter()
280                .map(|v| akar_common::arrow_vector::ArrowVector::from_legacy(v).array)
281                .collect::<Vec<_>>();
282            let arrow_field_types = fields.iter().map(|v| v.physical_type()).collect::<Vec<_>>();
283            DataChunk::new(arrow_fields, arrow_field_types)
284        }])
285    }
286
287    pub fn build_output(&self, groups: &hashbrown::HashMap<u64, Vec<(Value, Vec<AggValueState>)>>) -> OperatorResult {
288        if groups.is_empty() {
289            // Scalar aggregate (no GROUP BY) over empty input must still emit
290            // exactly one row with default values (COUNT=0, SUM/MIN/MAX/AVG=NULL).
291            if self.group_by_cols.is_empty() && !self.funcs.is_empty() {
292                return self.empty_scalar_result();
293            }
294            return self.empty_result();
295        }
296
297        // Flatten buckets into parallel arrays
298        let mut group_keys: Vec<Value> = Vec::new();
299        let mut agg_results: Vec<Vec<Value>> = (0..self.funcs.len()).map(|_| Vec::new()).collect();
300
301        for bucket in groups.values() {
302            for (key, states) in bucket {
303                group_keys.push(key.clone());
304                for (i, state) in states.iter().enumerate() {
305                    agg_results[i].push(state.finalize());
306                }
307            }
308        }
309
310        // Non-scalar aggregate results (e.g. `collect(DISTINCT x)` → Value::List)
311        // cannot be stored through `store_value_in_vector` (it marks List rows
312        // null). Build those columns directly as Arrow arrays (P88).
313        if agg_results
314            .iter()
315            .any(|r| r.iter().any(|v| matches!(v, Value::List(_))))
316        {
317            return self.build_output_direct(&group_keys, &agg_results);
318        }
319
320        let num_group_cols = self.group_by_cols.len();
321        let num_rows = group_keys.len();
322        let mut output = Vec::with_capacity(num_group_cols + self.funcs.len());
323
324        // Group key columns
325        if num_group_cols == 1 {
326            let first_val = &group_keys[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, key) in group_keys.iter().enumerate() {
331                if matches!(key, Value::Null) {
332                    v.set_null(row, true);
333                } else {
334                    store_value_in_vector(&mut v, row, key)?;
335                }
336            }
337            output.push(v);
338        } else {
339            for gc_idx in 0..num_group_cols {
340                let first_key = &group_keys[0];
341                let inner_val = match first_key {
342                    Value::List(vals) => vals.get(gc_idx).cloned().unwrap_or(Value::Null),
343                    _ => Value::Null,
344                };
345                let phys_type = inner_val.physical_type();
346                let mut v = ValueVector::new(phys_type, num_rows);
347                v.resize(num_rows);
348                for (row, key) in group_keys.iter().enumerate() {
349                    let val = match key {
350                        Value::List(vals) => vals.get(gc_idx).cloned().unwrap_or(Value::Null),
351                        _ => Value::Null,
352                    };
353                    if matches!(val, Value::Null) {
354                        v.set_null(row, true);
355                    } else {
356                        store_value_in_vector(&mut v, row, &val)?;
357                    }
358                }
359                output.push(v);
360            }
361        }
362
363        // Aggregate result columns
364        for i in 0..self.funcs.len() {
365            let first_val = &agg_results[i][0];
366            let phys_type = first_val.physical_type();
367            let mut v = ValueVector::new(phys_type, num_rows);
368            v.resize(num_rows);
369            for (row, val) in agg_results[i].iter().enumerate() {
370                store_value_in_vector(&mut v, row, val)?;
371            }
372            output.push(v);
373        }
374
375        // Split output into chunks of 2048
376        const CHUNK_SIZE: usize = 2048;
377        let mut chunks = Vec::new();
378
379        for chunk_start in (0..num_rows).step_by(CHUNK_SIZE) {
380            let chunk_end = (chunk_start + CHUNK_SIZE).min(num_rows);
381            let chunk_len = chunk_end - chunk_start;
382
383            let mut chunk_fields = Vec::with_capacity(output.len());
384            for field in &output {
385                let mut new_v = ValueVector::new(field.physical_type(), chunk_len);
386                new_v.resize(chunk_len);
387                for i in 0..chunk_len {
388                    if field.is_null(chunk_start + i) {
389                        new_v.set_null(i, true);
390                    } else if let Some(val) = field.get_value(chunk_start + i) {
391                        store_value_in_vector(&mut new_v, i, &val)?;
392                    }
393                }
394                chunk_fields.push(new_v);
395            }
396            chunks.push({
397                let arrow_fields = chunk_fields
398                    .iter()
399                    .map(|v| akar_common::arrow_vector::ArrowVector::from_legacy(v).array)
400                    .collect::<Vec<_>>();
401                let arrow_field_types = chunk_fields.iter().map(|v| v.physical_type()).collect::<Vec<_>>();
402                DataChunk::new(arrow_fields, arrow_field_types)
403            });
404        }
405
406        Ok(chunks)
407    }
408
409    /// Build aggregate output when a result column is non-scalar (Value::List,
410    /// e.g. `collect(DISTINCT x)`, P88) — `store_value_in_vector` cannot
411    /// represent lists. Group-key columns go through the scalar ValueVector
412    /// path; aggregate columns are built with `arrow_array_from_values`.
413    fn build_output_direct(&self, group_keys: &[Value], agg_results: &[Vec<Value>]) -> OperatorResult {
414        let num_rows = group_keys.len();
415        let num_group_cols = self.group_by_cols.len();
416        let mut arrow_fields = Vec::with_capacity(num_group_cols + self.funcs.len());
417        let mut arrow_types = Vec::with_capacity(num_group_cols + self.funcs.len());
418
419        for gc_idx in 0..num_group_cols {
420            let first_key = &group_keys[0];
421            let inner_val = match first_key {
422                Value::List(vals) if num_group_cols > 1 => vals.get(gc_idx).cloned().unwrap_or(Value::Null),
423                _ => first_key.clone(),
424            };
425            let phys_type = inner_val.physical_type();
426            let mut v = ValueVector::new(phys_type, num_rows);
427            v.resize(num_rows);
428            for (row, key) in group_keys.iter().enumerate() {
429                let val = match key {
430                    Value::List(vals) if num_group_cols > 1 => vals.get(gc_idx).cloned().unwrap_or(Value::Null),
431                    _ => key.clone(),
432                };
433                if matches!(val, Value::Null) {
434                    v.set_null(row, true);
435                } else {
436                    store_value_in_vector(&mut v, row, &val)?;
437                }
438            }
439            arrow_fields.push(akar_common::arrow_vector::ArrowVector::from_legacy(&v).array);
440            arrow_types.push(phys_type);
441        }
442
443        for results in agg_results {
444            arrow_fields.push(akar_common::arrow_vector::arrow_array_from_values(results));
445            let phys = if results.iter().any(|v| matches!(v, Value::List(_))) {
446                PhysicalTypeID::List
447            } else {
448                results
449                    .first()
450                    .map(|v| v.physical_type())
451                    .unwrap_or(PhysicalTypeID::Any)
452            };
453            arrow_types.push(phys);
454        }
455
456        Ok(vec![DataChunk::new(arrow_fields, arrow_types)])
457    }
458}
459
460/// Hash group key columns directly from Arrow arrays without creating intermediate `Value` objects.
461/// For strings, avoids the `to_string()` allocation that `get_value()` would incur.
462/// For primitives, avoids `Value` enum dispatch overhead.
463pub fn hash_group_key(chunk: &DataChunk, group_cols: &[u32], row: usize) -> u64 {
464    use std::hash::Hash;
465    use std::hash::Hasher;
466    let mut hasher = ahash::AHasher::default();
467    for &gc in group_cols {
468        let col = gc as usize;
469        if col >= chunk.fields.len() {
470            0u8.hash(&mut hasher);
471            continue;
472        }
473        if chunk.is_null(col, row) {
474            0u8.hash(&mut hasher);
475            continue;
476        }
477        match chunk.field_types[col] {
478            PhysicalTypeID::Int64 => {
479                let v = chunk.get_i64(col, row).unwrap_or(0);
480                v.hash(&mut hasher);
481            }
482            PhysicalTypeID::Int32 => {
483                let v = chunk.get_i32(col, row).unwrap_or(0);
484                v.hash(&mut hasher);
485            }
486            PhysicalTypeID::Double => {
487                let v = chunk.get_f64(col, row).unwrap_or(0.0);
488                v.to_bits().hash(&mut hasher);
489            }
490            PhysicalTypeID::Bool => {
491                let v = chunk.get_bool(col, row).unwrap_or(false);
492                v.hash(&mut hasher);
493            }
494            PhysicalTypeID::String => {
495                if let Some(s) = chunk.get_string(col, row) {
496                    s.hash(&mut hasher);
497                }
498            }
499            _ => {
500                // Fallback: create Value for types we don't handle directly
501                if let Some(val) = chunk.get_value(col, row) {
502                    hash_value_into(&val, &mut hasher);
503                }
504            }
505        }
506    }
507    hasher.finish()
508}
509
510/// Check if a stored group key matches the current row's group column values.
511/// Avoids creating Value::List or intermediate Value objects for comparison.
512pub fn keys_equal(stored: &Value, chunk: &DataChunk, group_cols: &[u32], row: usize) -> bool {
513    if group_cols.is_empty() {
514        // Scalar aggregate without GROUP BY: every row belongs to the single
515        // global group, so the stored `Value::Null` key always matches (P52.8).
516        // Without this, each row became its own bucket entry with an identical
517        // Null key, causing O(n^2) bucket scans and N duplicate output groups.
518        return true;
519    }
520    if group_cols.len() == 1 {
521        let col = group_cols[0] as usize;
522        if col >= chunk.fields.len() {
523            return *stored == Value::Null;
524        }
525        if chunk.is_null(col, row) {
526            return *stored == Value::Null;
527        }
528        return match stored {
529            Value::Int64(v) => chunk.get_i64(col, row).is_some_and(|x| x == *v),
530            Value::Int32(v) => chunk.get_i32(col, row).is_some_and(|x| x == *v),
531            Value::Double(v) => chunk.get_f64(col, row).is_some_and(|x| x == *v),
532            Value::Bool(v) => chunk.get_bool(col, row).is_some_and(|x| x == *v),
533            Value::String(v) => chunk.get_string(col, row).is_some_and(|x| x == *v),
534            _ => {
535                let val = chunk.get_value(col, row).unwrap_or(Value::Null);
536                *stored == val
537            }
538        };
539    }
540    match stored {
541        Value::List(vals) if vals.len() == group_cols.len() => vals.iter().enumerate().all(|(i, v)| {
542            let col = group_cols[i] as usize;
543            if col >= chunk.fields.len() {
544                return *v == Value::Null;
545            }
546            if chunk.is_null(col, row) {
547                return *v == Value::Null;
548            }
549            match v {
550                Value::Int64(expected) => chunk.get_i64(col, row).is_some_and(|x| x == *expected),
551                Value::Int32(expected) => chunk.get_i32(col, row).is_some_and(|x| x == *expected),
552                Value::Double(expected) => chunk.get_f64(col, row).is_some_and(|x| x == *expected),
553                Value::Bool(expected) => chunk.get_bool(col, row).is_some_and(|x| x == *expected),
554                Value::String(expected) => chunk.get_string(col, row).is_some_and(|x| x == *expected),
555                _ => {
556                    let val = chunk.get_value(col, row).unwrap_or(Value::Null);
557                    *v == val
558                }
559            }
560        }),
561        _ => false,
562    }
563}
564
565/// Build a composite group key from chunk columns.
566pub fn build_group_key(chunk: &DataChunk, group_cols: &[u32], row: usize) -> Value {
567    if group_cols.is_empty() {
568        return Value::Null;
569    }
570    if group_cols.len() == 1 {
571        chunk
572            .fields
573            .get(group_cols[0] as usize)
574            .map(|_| chunk.get_value(group_cols[0] as usize, row))
575            .unwrap_or(Some(Value::Null))
576            .unwrap_or(Value::Null)
577    } else {
578        let vals: Vec<Value> = group_cols
579            .iter()
580            .map(|&gc| {
581                chunk
582                    .fields
583                    .get(gc as usize)
584                    .map(|_| chunk.get_value(gc as usize, row))
585                    .unwrap_or(Some(Value::Null))
586                    .unwrap_or(Value::Null)
587            })
588            .collect();
589        Value::List(vals)
590    }
591}
592
593/// Resolve an expression name to a column index using flexible matching.
594/// Tries: exact match, numeric parse, ends_with(".name").
595fn resolve_name_to_col(name: &str, field_names: &[String]) -> Option<usize> {
596    if let Some(idx) = field_names.iter().position(|n| n == name) {
597        return Some(idx);
598    }
599    if let Ok(idx) = name.parse::<usize>() {
600        if idx < field_names.len() {
601            return Some(idx);
602        }
603    }
604    let dot_name = format!(".{}", name);
605    if let Some(idx) = field_names.iter().position(|n| n.ends_with(&dot_name)) {
606        return Some(idx);
607    }
608    None
609}
610
611/// Resolve aggregate function argument expressions to column indices.
612/// Returns one Option per function: None means no column needed (e.g., COUNT(*)).
613pub fn resolve_agg_col_indices(agg_expressions: &[Vec<Expression>], field_names: &[String]) -> Vec<Option<usize>> {
614    agg_expressions
615        .iter()
616        .map(|args| {
617            for expr in args {
618                match expr {
619                    Expression::Variable(name) => {
620                        if let Some(idx) = resolve_name_to_col(name, field_names) {
621                            return Some(idx);
622                        }
623                    }
624                    Expression::PropertyAccess(base, prop) => {
625                        if let Expression::Variable(prefix) = base.as_ref() {
626                            // Try "prefix.prop" (exact match with Cypher variable name)
627                            let qualified = format!("{}.{}", prefix, prop);
628                            if let Some(idx) = field_names.iter().position(|n| n == &qualified) {
629                                return Some(idx);
630                            }
631                            // Try ".prop" suffix match (handles table-prefixed names like "Person.age")
632                            let dot_prop = format!(".{}", prop);
633                            if let Some(idx) = field_names.iter().position(|n| n.ends_with(&dot_prop)) {
634                                return Some(idx);
635                            }
636                        }
637                    }
638                    Expression::Star => return None,
639                    _ => {}
640                }
641            }
642            None
643        })
644        .collect()
645}
646
647/// Resolve GROUP BY expressions to actual column indices using field_names.
648pub fn resolve_group_by_indices(group_by: &[Expression], field_names: &[String]) -> Vec<u32> {
649    group_by
650        .iter()
651        .map(|expr| match expr {
652            Expression::Variable(name) => resolve_name_to_col(name, field_names).unwrap_or(0) as u32,
653            Expression::PropertyAccess(base, prop) => {
654                if let Expression::Variable(prefix) = base.as_ref() {
655                    let qualified = format!("{}.{}", prefix, prop);
656                    if let Some(idx) = field_names.iter().position(|n| n == &qualified) {
657                        return idx as u32;
658                    }
659                    let dot_prop = format!(".{}", prop);
660                    if let Some(idx) = field_names.iter().position(|n| n.ends_with(&dot_prop)) {
661                        return idx as u32;
662                    }
663                }
664                0
665            }
666            _ => 0,
667        })
668        .collect()
669}
670
671/// Update aggregate states for a single row.
672pub fn update_states_row(
673    states: &mut [AggValueState],
674    chunk: &DataChunk,
675    funcs: &[AggregateFunction],
676    col_indices: &[Option<usize>],
677    row: usize,
678) {
679    for (i, state) in states.iter_mut().enumerate() {
680        if matches!(funcs[i], AggregateFunction::CountStar) {
681            if let AggValueState::Count(n) = state {
682                *n += 1;
683            }
684            continue;
685        }
686        if col_indices.get(i).copied().flatten().is_none() && matches!(funcs[i], AggregateFunction::Count) {
687            if let AggValueState::Count(n) = state {
688                *n += 1;
689            }
690            continue;
691        }
692        let col_idx = col_indices
693            .get(i)
694            .copied()
695            .flatten()
696            .unwrap_or_else(|| i.min(chunk.fields.len().saturating_sub(1)));
697        let val = chunk
698            .fields
699            .get(col_idx)
700            .map(|_| chunk.get_value(col_idx, row))
701            .unwrap_or(Some(Value::Null))
702            .unwrap_or(Value::Null);
703        state.update(&val);
704    }
705}
706
707/// Update aggregate states for a single row, honoring per-function DISTINCT
708/// flags (P88, `COUNT(DISTINCT x)` → name `COUNT_DISTINCT` + this flag).
709/// For DISTINCT functions, NULLs and argument values already seen for this
710/// (group, function) are skipped, so the state only ever accumulates distinct
711/// non-null values. Seen-values use a linear `Vec<Value>` scan — fine for the
712/// rarity of DISTINCT aggregates; dedicated fast paths cover the common cases.
713pub fn update_states_row_distinct(
714    states: &mut [AggValueState],
715    seen: &mut [Vec<Value>],
716    chunk: &DataChunk,
717    funcs: &[AggregateFunction],
718    col_indices: &[Option<usize>],
719    distinct_flags: &[bool],
720    row: usize,
721) {
722    for (i, state) in states.iter_mut().enumerate() {
723        if matches!(funcs[i], AggregateFunction::CountStar) {
724            if let AggValueState::Count(n) = state {
725                *n += 1;
726            }
727            continue;
728        }
729        if col_indices.get(i).copied().flatten().is_none() && matches!(funcs[i], AggregateFunction::Count) {
730            if let AggValueState::Count(n) = state {
731                *n += 1;
732            }
733            continue;
734        }
735        let col_idx = col_indices
736            .get(i)
737            .copied()
738            .flatten()
739            .unwrap_or_else(|| i.min(chunk.fields.len().saturating_sub(1)));
740        let val = chunk
741            .fields
742            .get(col_idx)
743            .map(|_| chunk.get_value(col_idx, row))
744            .unwrap_or(Some(Value::Null))
745            .unwrap_or(Value::Null);
746        if distinct_flags[i] {
747            // DISTINCT aggregates ignore NULLs (COUNT(DISTINCT x) counts only
748            // distinct non-null x) and already-seen values within the group.
749            if matches!(val, Value::Null) {
750                continue;
751            }
752            let seen_list = &mut seen[i];
753            if seen_list.contains(&val) {
754                continue;
755            }
756            seen_list.push(val.clone());
757        }
758        state.update(&val);
759    }
760}
761
762/// Compute a scalar aggregate (Sum/Min/Max/Avg) over chunks using Arrow compute kernels.
763/// Avoids per-row Value dispatch entirely.
764fn arrow_scalar_agg(func: &AggregateFunction, chunks: &[DataChunk], col_idx: usize) -> Value {
765    // Collect non-null numeric values from all chunks into a single Arrow array
766    let all_values: Vec<Value> = chunks
767        .iter()
768        .flat_map(|c| {
769            if col_idx >= c.fields.len() {
770                return Vec::new();
771            }
772            let field = &c.fields[col_idx];
773            let rows = if c.sel_vector.is_some() {
774                c.iter_rows().collect::<Vec<_>>()
775            } else {
776                (0..c.size).collect::<Vec<_>>()
777            };
778            rows.into_iter()
779                .filter_map(|row| {
780                    if field.is_null(row) {
781                        None
782                    } else {
783                        c.get_value(col_idx, row)
784                    }
785                })
786                .collect::<Vec<_>>()
787        })
788        .collect();
789
790    if all_values.is_empty() {
791        return match func {
792            AggregateFunction::Sum | AggregateFunction::Avg => Value::Null,
793            AggregateFunction::Min | AggregateFunction::Max => Value::Null,
794            _ => Value::Null,
795        };
796    }
797
798    // Try to use Arrow compute kernels on primitive arrays
799    if let Some(arr) = values_to_prim_array(&all_values) {
800        return match func {
801            AggregateFunction::Sum => compute_sum(&arr),
802            AggregateFunction::Min => compute_min(&arr),
803            AggregateFunction::Max => compute_max(&arr),
804            AggregateFunction::Avg => {
805                let sum_val = compute_sum_f64(&arr);
806                let count = non_null_count(chunks, col_idx);
807                if count == 0 {
808                    Value::Null
809                } else {
810                    Value::Double(sum_val / count as f64)
811                }
812            }
813            _ => Value::Null,
814        };
815    }
816
817    // Fallback: per-row Value dispatch (for non-numeric types)
818    let mut state = AggValueState::new(func);
819    for val in &all_values {
820        state.update(val);
821    }
822    state.finalize()
823}
824
825enum PrimArray {
826    I64(arrow::array::PrimitiveArray<Int64Type>),
827    I32(arrow::array::PrimitiveArray<Int32Type>),
828    F64(arrow::array::PrimitiveArray<Float64Type>),
829}
830
831fn values_to_prim_array(vals: &[Value]) -> Option<PrimArray> {
832    match &vals[0] {
833        Value::Int64(_) => {
834            let mut b = arrow::array::Int64Builder::with_capacity(vals.len());
835            for v in vals {
836                match v {
837                    Value::Int64(n) => b.append_value(*n),
838                    Value::Null => b.append_null(),
839                    _ => return None,
840                }
841            }
842            Some(PrimArray::I64(b.finish()))
843        }
844        Value::Int32(_) => {
845            let mut b = arrow::array::Int32Builder::with_capacity(vals.len());
846            for v in vals {
847                match v {
848                    Value::Int32(n) => b.append_value(*n),
849                    Value::Null => b.append_null(),
850                    _ => return None,
851                }
852            }
853            Some(PrimArray::I32(b.finish()))
854        }
855        Value::Double(_) => {
856            let mut b = arrow::array::Float64Builder::with_capacity(vals.len());
857            for v in vals {
858                match v {
859                    Value::Double(n) => b.append_value(*n),
860                    Value::Null => b.append_null(),
861                    _ => return None,
862                }
863            }
864            Some(PrimArray::F64(b.finish()))
865        }
866        _ => None,
867    }
868}
869
870fn compute_sum(arr: &PrimArray) -> Value {
871    match arr {
872        PrimArray::I64(a) => compute::sum(a).map(Value::Int64).unwrap_or(Value::Null),
873        PrimArray::I32(a) => compute::sum(a).map(|v| Value::Int64(v as i64)).unwrap_or(Value::Null),
874        PrimArray::F64(a) => compute::sum(a).map(Value::Double).unwrap_or(Value::Null),
875    }
876}
877
878fn compute_sum_f64(arr: &PrimArray) -> f64 {
879    match arr {
880        PrimArray::I64(a) => compute::sum::<Int64Type>(a).unwrap_or(0) as f64,
881        PrimArray::I32(a) => compute::sum::<Int32Type>(a).unwrap_or(0) as f64,
882        PrimArray::F64(a) => compute::sum::<Float64Type>(a).unwrap_or(0.0),
883    }
884}
885
886fn compute_min(arr: &PrimArray) -> Value {
887    match arr {
888        PrimArray::I64(a) => compute::min(a).map(Value::Int64).unwrap_or(Value::Null),
889        PrimArray::I32(a) => compute::min(a).map(|v| Value::Int64(v as i64)).unwrap_or(Value::Null),
890        PrimArray::F64(a) => compute::min(a).map(Value::Double).unwrap_or(Value::Null),
891    }
892}
893
894fn compute_max(arr: &PrimArray) -> Value {
895    match arr {
896        PrimArray::I64(a) => compute::max(a).map(Value::Int64).unwrap_or(Value::Null),
897        PrimArray::I32(a) => compute::max(a).map(|v| Value::Int64(v as i64)).unwrap_or(Value::Null),
898        PrimArray::F64(a) => compute::max(a).map(Value::Double).unwrap_or(Value::Null),
899    }
900}
901
902fn non_null_count(chunks: &[DataChunk], col_idx: usize) -> u64 {
903    let mut count = 0u64;
904    for c in chunks {
905        if col_idx >= c.fields.len() {
906            continue;
907        }
908        let field = &c.fields[col_idx];
909        if c.sel_vector.is_some() {
910            for row in c.iter_rows() {
911                if !field.is_null(row) {
912                    count += 1;
913                }
914            }
915        } else {
916            count += (field.len() - field.null_count()) as u64;
917        }
918    }
919    count
920}