Skip to main content

oxirs_arq/
parallel_executor_engine.rs

1//! Parallel query executor core: thread pool, algebra dispatch, and the
2//! BGP / join / union / filter / order-by / group-by execution paths.
3//!
4//! This module defines [`ParallelQueryExecutor`] and the bulk of its query
5//! evaluation methods. Property-path, optional/minus, and projection-style
6//! operators live in the sibling [`crate::parallel_executor_ops`] module;
7//! the scan iterator and work-stealing queue live in
8//! [`crate::parallel_executor_queue`].
9
10use crate::algebra::{
11    Aggregate, Algebra, Binding, Expression, Literal, PropertyPath, Solution, Term as AlgebraTerm,
12    TriplePattern, Variable,
13};
14use crate::executor::stats::ExecutionStats;
15use crate::executor::{Dataset, ExecutionContext, ParallelConfig};
16use crate::expression::ExpressionEvaluator;
17use crate::parallel_types::ParallelStats;
18use crate::term::{BindingContext, Term};
19use anyhow::{anyhow, Result};
20use dashmap::DashMap;
21use oxirs_core::model::NamedNode;
22use parking_lot::RwLock;
23use rayon::prelude::*;
24use std::collections::{HashMap, HashSet};
25use std::sync::Arc;
26use std::time::Instant;
27
28/// Parallel query executor with advanced features
29pub struct ParallelQueryExecutor {
30    pub(crate) config: ParallelConfig,
31    pub(crate) stats: Arc<RwLock<ParallelStats>>,
32    pub(crate) thread_pool: rayon::ThreadPool,
33}
34
35/// Type alias for backward compatibility
36pub type ParallelExecutor = ParallelQueryExecutor;
37
38/// Collapse a length-one `PropertyPath::Iri` predicate encoding to a plain
39/// `Term::Iri`, mirroring the store's predicate handling. Other terms pass
40/// through unchanged. Used when re-verifying a bound pattern term against a
41/// stored value so the parser's property-path predicate encoding matches the
42/// store's plain-IRI representation.
43fn normalize_bound_term(term: &AlgebraTerm) -> AlgebraTerm {
44    match term {
45        AlgebraTerm::PropertyPath(PropertyPath::Iri(n)) => AlgebraTerm::Iri(n.clone()),
46        other => other.clone(),
47    }
48}
49
50impl ParallelQueryExecutor {
51    /// Create a new parallel query executor
52    pub fn new(config: ParallelConfig) -> Result<Self> {
53        let thread_pool = rayon::ThreadPoolBuilder::new()
54            .num_threads(config.max_threads)
55            .thread_name(|idx| format!("oxirs-arq-worker-{idx}"))
56            .stack_size(
57                config
58                    .thread_pool_config
59                    .stack_size
60                    .unwrap_or(8 * 1024 * 1024),
61            )
62            .build()
63            .map_err(|e| anyhow!("Failed to create thread pool: {}", e))?;
64
65        Ok(Self {
66            config,
67            stats: Arc::new(RwLock::new(ParallelStats::default())),
68            thread_pool,
69        })
70    }
71
72    /// Execute algebra expression in parallel
73    pub fn execute(
74        &self,
75        algebra: &Algebra,
76        dataset: &dyn Dataset,
77        context: &ExecutionContext,
78        stats: &mut ExecutionStats,
79    ) -> Result<Solution> {
80        let start = Instant::now();
81
82        // Update parallel stats
83        {
84            let mut pstats = self.stats.write();
85            pstats.parallel_operations += 1;
86        }
87
88        let result = self
89            .thread_pool
90            .install(|| self.execute_parallel_internal(algebra, dataset, context, stats))?;
91
92        // Calculate speedup
93        let _parallel_time = start.elapsed();
94        {
95            let mut pstats = self.stats.write();
96            pstats.thread_utilization = self.calculate_thread_utilization();
97        }
98
99        Ok(result)
100    }
101
102    /// Internal parallel execution method
103    pub(crate) fn execute_parallel_internal(
104        &self,
105        algebra: &Algebra,
106        dataset: &dyn Dataset,
107        context: &ExecutionContext,
108        stats: &mut ExecutionStats,
109    ) -> Result<Solution> {
110        match algebra {
111            Algebra::Bgp(patterns) => self.execute_parallel_bgp(patterns, dataset, stats),
112            Algebra::Join { left, right } => {
113                self.execute_parallel_join(left, right, dataset, context, stats)
114            }
115            Algebra::Union { left, right } => {
116                self.execute_parallel_union(left, right, dataset, context, stats)
117            }
118            Algebra::Filter { pattern, condition } => {
119                self.execute_parallel_filter(pattern, condition, dataset, context, stats)
120            }
121            Algebra::OrderBy {
122                pattern,
123                conditions,
124            } => {
125                let conditions_tuple: Vec<(Expression, bool)> = conditions
126                    .iter()
127                    .map(|c| (c.expr.clone(), c.ascending))
128                    .collect();
129                self.execute_parallel_order_by(pattern, &conditions_tuple, dataset, context, stats)
130            }
131            Algebra::Group {
132                pattern,
133                variables,
134                aggregates,
135            } => {
136                let group_vars: Vec<Variable> = variables
137                    .iter()
138                    .filter_map(|gc| {
139                        gc.alias.clone().or_else(|| {
140                            if let Expression::Variable(var) = &gc.expr {
141                                Some(var.clone())
142                            } else {
143                                None
144                            }
145                        })
146                    })
147                    .collect();
148                self.execute_parallel_group(
149                    pattern,
150                    &group_vars,
151                    aggregates,
152                    dataset,
153                    context,
154                    stats,
155                )
156            }
157            Algebra::PropertyPath {
158                subject,
159                path,
160                object,
161            } => {
162                self.execute_parallel_property_path(subject, path, object, dataset, context, stats)
163            }
164            Algebra::LeftJoin {
165                left,
166                right,
167                filter,
168            } => self.execute_parallel_left_join(left, right, filter, dataset, context, stats),
169            Algebra::Extend {
170                pattern,
171                variable,
172                expr,
173            } => self.execute_parallel_extend(pattern, variable, expr, dataset, context, stats),
174            Algebra::Minus { left, right } => {
175                self.execute_parallel_minus(left, right, dataset, context, stats)
176            }
177            Algebra::Service {
178                endpoint,
179                pattern,
180                silent,
181            } => self.execute_parallel_service(endpoint, pattern, *silent, dataset, context, stats),
182            Algebra::Graph { graph, pattern } => {
183                self.execute_parallel_graph(graph, pattern, dataset, context, stats)
184            }
185            Algebra::Project { pattern, variables } => {
186                self.execute_parallel_project(pattern, variables, dataset, context, stats)
187            }
188            Algebra::Distinct { pattern } => {
189                self.execute_parallel_distinct(pattern, dataset, context, stats)
190            }
191            Algebra::Reduced { pattern } => {
192                self.execute_parallel_reduced(pattern, dataset, context, stats)
193            }
194            Algebra::Slice {
195                pattern,
196                offset,
197                limit,
198            } => self.execute_parallel_slice(pattern, *offset, *limit, dataset, context, stats),
199            // Dataset-independent leaves: evaluating them needs no
200            // parallelism, but erroring out made any query CONTAINING them
201            // fail under the Parallel strategy where Serial succeeds.
202            Algebra::Values { bindings, .. } => Ok(bindings.clone()),
203            Algebra::Table => Ok(vec![Binding::new()]),
204            Algebra::Zero | Algebra::Empty => Ok(Vec::new()),
205            _ => {
206                // Fall back to sequential execution for truly unsupported operations
207                Err(anyhow!(
208                    "Parallel execution not supported for this algebra type"
209                ))
210            }
211        }
212    }
213
214    /// Execute BGP in parallel with partition-based scanning
215    pub(crate) fn execute_parallel_bgp(
216        &self,
217        patterns: &[TriplePattern],
218        dataset: &dyn Dataset,
219        stats: &mut ExecutionStats,
220    ) -> Result<Solution> {
221        if patterns.is_empty() {
222            return Ok(vec![HashMap::new()]);
223        }
224
225        // BGP patterns are CONJUNCTIVE: they must be joined, not concatenated.
226        // The previous implementation split the pattern list into per-thread
227        // chunks and then `merge_bgp_results`-CONCATENATED the per-chunk
228        // solutions, which returned the union of each pattern's bindings instead
229        // of their join — a silent wrong answer for every multi-pattern BGP
230        // (e.g. `?s :p ?o . ?s :q ?o2` yielded the 5 unjoined rows rather than
231        // the 2 joined ones). Fold the whole pattern list in sequence instead;
232        // per-pattern parallelism is preserved by `join_with_pattern_parallel`,
233        // which fans the current binding set across the thread pool.
234        let solution = self.process_bgp_chunk(patterns, dataset)?;
235        self.merge_bgp_results(vec![solution], stats)
236    }
237
238    /// Process a chunk of BGP patterns
239    fn process_bgp_chunk(
240        &self,
241        patterns: &[TriplePattern],
242        dataset: &dyn Dataset,
243    ) -> Result<Solution> {
244        let mut solution = vec![HashMap::new()];
245
246        for pattern in patterns {
247            solution = self.join_with_pattern_parallel(solution, pattern, dataset)?;
248            if solution.is_empty() {
249                break;
250            }
251        }
252
253        Ok(solution)
254    }
255
256    /// Join solution with pattern in parallel
257    fn join_with_pattern_parallel(
258        &self,
259        solution: Solution,
260        pattern: &TriplePattern,
261        dataset: &dyn Dataset,
262    ) -> Result<Solution> {
263        // Use parallel iterator for large solutions
264        if solution.len() > self.config.parallel_threshold {
265            // Propagate scan errors rather than `.unwrap_or_default()` them: a
266            // dropped `Err` here would silently shrink the BGP result (a wrong
267            // `200 OK`) exactly on the large-input path where it matters most.
268            let results: Vec<Binding> = solution
269                .par_iter()
270                .map(|binding| self.extend_binding_with_pattern(binding, pattern, dataset))
271                .collect::<Result<Vec<Vec<Binding>>>>()?
272                .into_iter()
273                .flatten()
274                .collect();
275            Ok(results)
276        } else {
277            // Sequential for small solutions
278            let mut result = Vec::new();
279            for binding in solution {
280                let extensions = self.extend_binding_with_pattern(&binding, pattern, dataset)?;
281                result.extend(extensions);
282            }
283            Ok(result)
284        }
285    }
286
287    /// Extend binding with pattern matches
288    fn extend_binding_with_pattern(
289        &self,
290        binding: &Binding,
291        pattern: &TriplePattern,
292        dataset: &dyn Dataset,
293    ) -> Result<Vec<Binding>> {
294        let instantiated = self.instantiate_pattern(pattern, binding);
295        let triples = dataset.find_triples(&instantiated)?;
296
297        let mut results = Vec::new();
298        for (s, p, o) in triples {
299            if let Some(new_binding) = self.try_extend_binding(binding, pattern, &s, &p, &o) {
300                results.push(new_binding);
301            }
302        }
303
304        Ok(results)
305    }
306
307    /// Instantiate pattern with bindings
308    fn instantiate_pattern(&self, pattern: &TriplePattern, binding: &Binding) -> TriplePattern {
309        TriplePattern {
310            subject: self.instantiate_term(&pattern.subject, binding),
311            predicate: self.instantiate_term(&pattern.predicate, binding),
312            object: self.instantiate_term(&pattern.object, binding),
313        }
314    }
315
316    /// Instantiate term with binding
317    fn instantiate_term(&self, term: &AlgebraTerm, binding: &Binding) -> AlgebraTerm {
318        match term {
319            AlgebraTerm::Variable(var) => binding.get(var).cloned().unwrap_or_else(|| term.clone()),
320            _ => term.clone(),
321        }
322    }
323
324    /// Try to extend binding with new values
325    fn try_extend_binding(
326        &self,
327        binding: &Binding,
328        pattern: &TriplePattern,
329        s: &AlgebraTerm,
330        p: &AlgebraTerm,
331        o: &AlgebraTerm,
332    ) -> Option<Binding> {
333        let mut new_binding = binding.clone();
334
335        if !self.try_bind(&mut new_binding, &pattern.subject, s)
336            || !self.try_bind(&mut new_binding, &pattern.predicate, p)
337            || !self.try_bind(&mut new_binding, &pattern.object, o)
338        {
339            return None;
340        }
341
342        Some(new_binding)
343    }
344
345    /// Try to bind variable to value
346    fn try_bind(
347        &self,
348        binding: &mut Binding,
349        pattern_term: &AlgebraTerm,
350        value: &AlgebraTerm,
351    ) -> bool {
352        match pattern_term {
353            AlgebraTerm::Variable(var) => {
354                if let Some(existing) = binding.get(var) {
355                    existing == value
356                } else {
357                    binding.insert(var.clone(), value.clone());
358                    true
359                }
360            }
361            // A bound (non-variable) term must equal the stored value. The parser
362            // encodes a single-IRI predicate as a length-one property path
363            // (`PropertyPath::Iri`), whereas the store returns it as a plain
364            // `Iri`; normalizing that encoding before comparison mirrors the
365            // Serial path (`execute_pattern_with_dataset` trusts `find_triples`
366            // and only binds variables). Without this, every bound-predicate BGP
367            // silently returned zero rows under the Parallel strategy.
368            _ => normalize_bound_term(pattern_term) == normalize_bound_term(value),
369        }
370    }
371
372    /// Merge BGP results from parallel execution
373    pub(crate) fn merge_bgp_results(
374        &self,
375        partial_results: Vec<Solution>,
376        stats: &mut ExecutionStats,
377    ) -> Result<Solution> {
378        // Use parallel reduction for merging
379        let merged = partial_results
380            .into_par_iter()
381            .reduce(Vec::new, |mut acc, mut partial| {
382                acc.append(&mut partial);
383                acc
384            });
385
386        stats.intermediate_results += merged.len();
387        Ok(merged)
388    }
389
390    /// Execute parallel hash join
391    fn execute_parallel_join(
392        &self,
393        left: &Algebra,
394        right: &Algebra,
395        dataset: &dyn Dataset,
396        context: &ExecutionContext,
397        stats: &mut ExecutionStats,
398    ) -> Result<Solution> {
399        // Execute left and right sequentially to avoid borrowing issues with stats
400        let left_solution = self.execute_parallel_internal(left, dataset, context, stats)?;
401        let right_solution = self.execute_parallel_internal(right, dataset, context, stats)?;
402
403        // Find join variables
404        let join_vars = self.find_join_variables(&left_solution, &right_solution);
405
406        if join_vars.is_empty() {
407            // Cartesian product
408            self.parallel_cartesian_product(left_solution, right_solution, stats)
409        } else {
410            // Hash join
411            self.parallel_hash_join(left_solution, right_solution, join_vars, stats)
412        }
413    }
414
415    /// Find common variables between solutions
416    fn find_join_variables(&self, left: &Solution, right: &Solution) -> Vec<Variable> {
417        if left.is_empty() || right.is_empty() {
418            return vec![];
419        }
420
421        let left_vars: HashSet<_> = left[0].keys().cloned().collect();
422        let right_vars: HashSet<_> = right[0].keys().cloned().collect();
423
424        left_vars.intersection(&right_vars).cloned().collect()
425    }
426
427    /// Parallel hash join implementation
428    fn parallel_hash_join(
429        &self,
430        left: Solution,
431        right: Solution,
432        join_vars: Vec<Variable>,
433        stats: &mut ExecutionStats,
434    ) -> Result<Solution> {
435        // Build hash table from smaller side in parallel
436        let (build_side, probe_side) = if left.len() <= right.len() {
437            (left, right)
438        } else {
439            (right, left)
440        };
441
442        // Parallel hash table construction using DashMap
443        let hash_table: DashMap<Vec<AlgebraTerm>, Vec<Binding>> = DashMap::new();
444
445        // Keys are variable-length (filter_map, NO completeness gate),
446        // mirroring the Serial hash_join contract: a row whose join variable
447        // is unbound (heterogeneous UNION/OPTIONAL output) lands in the
448        // shorter-key bucket and still merges with its compatible partners —
449        // the old `key.len() == join_vars.len()` gate silently dropped it.
450        build_side.par_iter().for_each(|binding| {
451            let key: Vec<AlgebraTerm> = join_vars
452                .iter()
453                .filter_map(|var| binding.get(var).cloned())
454                .collect();
455            hash_table.entry(key).or_default().push(binding.clone());
456        });
457
458        // Parallel probing
459        let result: Vec<Binding> = probe_side
460            .par_iter()
461            .flat_map(|probe_binding| {
462                let key: Vec<AlgebraTerm> = join_vars
463                    .iter()
464                    .filter_map(|var| probe_binding.get(var).cloned())
465                    .collect();
466
467                match hash_table.get(&key) {
468                    Some(matches) => matches
469                        .iter()
470                        .filter_map(|build_binding| {
471                            self.merge_bindings(build_binding, probe_binding)
472                        })
473                        .collect::<Vec<_>>(),
474                    _ => {
475                        vec![]
476                    }
477                }
478            })
479            .collect();
480
481        stats.intermediate_results += result.len();
482        Ok(result)
483    }
484
485    /// Merge two bindings
486    pub(crate) fn merge_bindings(&self, left: &Binding, right: &Binding) -> Option<Binding> {
487        let mut merged = left.clone();
488
489        for (var, value) in right {
490            if let Some(existing) = merged.get(var) {
491                if existing != value {
492                    return None;
493                }
494            } else {
495                merged.insert(var.clone(), value.clone());
496            }
497        }
498
499        Some(merged)
500    }
501
502    /// Parallel cartesian product
503    fn parallel_cartesian_product(
504        &self,
505        left: Solution,
506        right: Solution,
507        stats: &mut ExecutionStats,
508    ) -> Result<Solution> {
509        let result: Vec<Binding> = left
510            .par_iter()
511            .flat_map(|l| {
512                right
513                    .iter()
514                    .filter_map(|r| self.merge_bindings(l, r))
515                    .collect::<Vec<_>>()
516            })
517            .collect();
518
519        stats.intermediate_results += result.len();
520        Ok(result)
521    }
522
523    /// Execute parallel union
524    fn execute_parallel_union(
525        &self,
526        left: &Algebra,
527        right: &Algebra,
528        dataset: &dyn Dataset,
529        context: &ExecutionContext,
530        stats: &mut ExecutionStats,
531    ) -> Result<Solution> {
532        // Execute both branches sequentially to avoid borrowing issues with stats
533        let left_result = self.execute_parallel_internal(left, dataset, context, stats)?;
534        let right_result = self.execute_parallel_internal(right, dataset, context, stats)?;
535
536        // SPARQL UNION has bag semantics: duplicate rows (within or across
537        // branches) must survive unless the query says DISTINCT. The previous
538        // parallel_distinct call here silently collapsed them, diverging from
539        // the Serial path's plain concatenation.
540        let mut result = left_result;
541        result.extend(right_result);
542
543        stats.intermediate_results += result.len();
544        Ok(result)
545    }
546
547    /// Parallel distinct operation
548    pub(crate) fn parallel_distinct(&self, solution: Solution) -> Solution {
549        // Structured, variable-sorted key — a Display-based joined string
550        // ("var=term||…") can alias two different bindings when a term itself
551        // contains the separator.
552        let seen: DashMap<Vec<(crate::algebra::Variable, String)>, ()> = DashMap::new();
553
554        solution
555            .into_par_iter()
556            .filter(|binding| {
557                let mut key: Vec<_> = binding
558                    .iter()
559                    .map(|(var, term)| (var.clone(), format!("{term:?}")))
560                    .collect();
561                key.sort();
562                seen.insert(key, ()).is_none()
563            })
564            .collect()
565    }
566
567    /// Execute parallel filter
568    fn execute_parallel_filter(
569        &self,
570        pattern: &Algebra,
571        condition: &Expression,
572        dataset: &dyn Dataset,
573        context: &ExecutionContext,
574        stats: &mut ExecutionStats,
575    ) -> Result<Solution> {
576        let solution = self.execute_parallel_internal(pattern, dataset, context, stats)?;
577
578        // Create expression evaluator for filtering
579        let extension_registry = context.extension_registry.clone();
580
581        // Parallel filtering. A whole-query fault (a typed `UnknownFunctionError`
582        // or a runtime `BudgetExceeded`) MUST propagate rather than be swallowed
583        // to `false` — silently dropping the offending rows would return a
584        // wrongly-shrunk `200 OK`, mirroring the Serial `apply_filter` contract.
585        // Every other error class (unbound variable, type error, …) is a per-row
586        // §17.3 evaluation error that excludes just that row. The closure returns
587        // `Result<Option<Binding>>` so the `Err` case can escape the parallel
588        // iterator via `collect::<Result<_>>()`.
589        //
590        // NOTE (remaining divergence): this evaluator is
591        // `crate::expression::ExpressionEvaluator`, a *distinct* implementation
592        // from Serial's dataset-aware `QueryExecutor::evaluate_expression`. It
593        // cannot evaluate `EXISTS` / `NOT EXISTS` (no dataset access on the rayon
594        // worker) and raises an *untyped* "Unknown function" error, so those two
595        // cases still diverge from Serial. Fully unifying the parallel filter
596        // onto the Serial evaluator is tracked separately.
597        let filtered: Vec<Binding> = solution
598            .into_par_iter()
599            .map(|binding| -> Result<Option<Binding>> {
600                let mut ctx = BindingContext::new();
601                for (var, term) in &binding {
602                    ctx.bind(var.as_str(), Term::from_algebra_term(term));
603                }
604                let evaluator_with_ctx =
605                    ExpressionEvaluator::with_context(extension_registry.clone(), ctx);
606                match evaluator_with_ctx.evaluate(condition) {
607                    Ok(term) => {
608                        if term.effective_boolean_value().unwrap_or(false) {
609                            Ok(Some(binding))
610                        } else {
611                            Ok(None)
612                        }
613                    }
614                    Err(e) => {
615                        if e.downcast_ref::<crate::executor::UnknownFunctionError>()
616                            .is_some()
617                            || e.downcast_ref::<crate::query_governor::BudgetExceeded>()
618                                .is_some()
619                        {
620                            Err(e)
621                        } else {
622                            Ok(None)
623                        }
624                    }
625                }
626            })
627            .collect::<Result<Vec<Option<Binding>>>>()?
628            .into_iter()
629            .flatten()
630            .collect();
631
632        stats.intermediate_results += filtered.len();
633        Ok(filtered)
634    }
635
636    /// Execute parallel order by
637    fn execute_parallel_order_by(
638        &self,
639        pattern: &Algebra,
640        conditions: &[(Expression, bool)], // (expr, ascending)
641        dataset: &dyn Dataset,
642        context: &ExecutionContext,
643        stats: &mut ExecutionStats,
644    ) -> Result<Solution> {
645        let mut solution = self.execute_parallel_internal(pattern, dataset, context, stats)?;
646
647        // Clone extension registry for use in closure
648        let extension_registry = context.extension_registry.clone();
649
650        // Parallel sort with custom comparator
651        solution.par_sort_by(|a, b| {
652            for (expr, ascending) in conditions {
653                // Create binding contexts
654                let mut ctx_a = BindingContext::new();
655                let mut ctx_b = BindingContext::new();
656                for (var, term) in a {
657                    ctx_a.bind(var.as_str(), Term::from_algebra_term(term));
658                }
659                for (var, term) in b {
660                    ctx_b.bind(var.as_str(), Term::from_algebra_term(term));
661                }
662
663                let evaluator_a =
664                    ExpressionEvaluator::with_context(extension_registry.clone(), ctx_a);
665                let evaluator_b =
666                    ExpressionEvaluator::with_context(extension_registry.clone(), ctx_b);
667
668                let val_a = evaluator_a.evaluate(expr).ok();
669                let val_b = evaluator_b.evaluate(expr).ok();
670
671                match (val_a, val_b) {
672                    (Some(a_term), Some(b_term)) => {
673                        let alg_a = a_term.to_algebra_term();
674                        let alg_b = b_term.to_algebra_term();
675                        // Literal pairs use the Serial comparator (numeric
676                        // partition ordered by value); the previous
677                        // Term::cmp path ordered by datatype IRI first, so
678                        // "10"^^xsd:decimal sorted before "5"^^xsd:integer.
679                        let cmp = match (&alg_a, &alg_b) {
680                            (AlgebraTerm::Literal(la), AlgebraTerm::Literal(lb)) => {
681                                crate::executor::queryexecutor_apply_order_by_group::compare_literals(la, lb)
682                            }
683                            (AlgebraTerm::Iri(ia), AlgebraTerm::Iri(ib)) => {
684                                ia.as_str().cmp(ib.as_str())
685                            }
686                            _ => self.compare_algebra_terms(&alg_a, &alg_b),
687                        };
688                        if cmp != std::cmp::Ordering::Equal {
689                            return if *ascending { cmp } else { cmp.reverse() };
690                        }
691                    }
692                    // SPARQL §15.1: unbound/error keys rank lowest, i.e.
693                    // first ascending and last descending.
694                    (Some(_), None) => {
695                        return if *ascending {
696                            std::cmp::Ordering::Greater
697                        } else {
698                            std::cmp::Ordering::Less
699                        }
700                    }
701                    (None, Some(_)) => {
702                        return if *ascending {
703                            std::cmp::Ordering::Less
704                        } else {
705                            std::cmp::Ordering::Greater
706                        }
707                    }
708                    (None, None) => continue,
709                }
710            }
711            std::cmp::Ordering::Equal
712        });
713
714        Ok(solution)
715    }
716
717    /// Compare algebra terms for ordering
718    pub(crate) fn compare_algebra_terms(
719        &self,
720        a: &AlgebraTerm,
721        b: &AlgebraTerm,
722    ) -> std::cmp::Ordering {
723        // Convert to internal terms for proper comparison
724        let term_a = Term::from_algebra_term(a);
725        let term_b = Term::from_algebra_term(b);
726        term_a
727            .partial_cmp(&term_b)
728            .unwrap_or(std::cmp::Ordering::Equal)
729    }
730
731    /// Execute parallel group by with aggregation
732    fn execute_parallel_group(
733        &self,
734        pattern: &Algebra,
735        variables: &[Variable],
736        aggregates: &[(Variable, Aggregate)],
737        dataset: &dyn Dataset,
738        context: &ExecutionContext,
739        stats: &mut ExecutionStats,
740    ) -> Result<Solution> {
741        let solution = self.execute_parallel_internal(pattern, dataset, context, stats)?;
742
743        // Parallel grouping using DashMap
744        let groups: DashMap<Vec<AlgebraTerm>, Vec<Binding>> = DashMap::new();
745
746        solution.par_iter().for_each(|binding| {
747            let key: Vec<AlgebraTerm> = variables
748                .iter()
749                .map(|var| {
750                    binding
751                        .get(var)
752                        .cloned()
753                        .unwrap_or(AlgebraTerm::Variable(var.clone()))
754                })
755                .collect();
756
757            groups.entry(key).or_default().push(binding.clone());
758        });
759
760        // Parallel aggregation - convert DashMap to Vec for parallel iteration
761        let groups_vec: Vec<(Vec<AlgebraTerm>, Vec<Binding>)> = groups.into_iter().collect();
762
763        let result: Vec<Binding> = groups_vec
764            .into_par_iter()
765            .map(|(key, group)| {
766                let mut result_binding = HashMap::new();
767
768                // Add grouping variables
769                for (i, var) in variables.iter().enumerate() {
770                    if let Some(term) = key.get(i) {
771                        if !matches!(term, AlgebraTerm::Variable(_)) {
772                            result_binding.insert(var.clone(), term.clone());
773                        }
774                    }
775                }
776
777                // Compute aggregates
778                for (var, agg) in aggregates {
779                    if let Ok(value) = self.compute_aggregate(agg, &group, context) {
780                        result_binding.insert(var.clone(), value);
781                    }
782                }
783
784                result_binding
785            })
786            .collect();
787
788        stats.intermediate_results += result.len();
789        Ok(result)
790    }
791
792    /// Compute aggregate value
793    fn compute_aggregate(
794        &self,
795        aggregate: &Aggregate,
796        group: &[Binding],
797        context: &ExecutionContext,
798    ) -> Result<AlgebraTerm> {
799        match aggregate {
800            Aggregate::Count { expr, distinct } => {
801                let values =
802                    self.collect_aggregate_values(expr.as_ref(), group, *distinct, context)?;
803                Ok(AlgebraTerm::Literal(Literal::typed(
804                    values.len().to_string(),
805                    NamedNode::new_unchecked("http://www.w3.org/2001/XMLSchema#integer"),
806                )))
807            }
808            Aggregate::Sum { expr, distinct } => {
809                let values =
810                    self.collect_aggregate_values(Some(expr), group, *distinct, context)?;
811                let sum = self.sum_numeric_values(values)?;
812                Ok(sum)
813            }
814            Aggregate::Min { expr, distinct } => {
815                let values =
816                    self.collect_aggregate_values(Some(expr), group, *distinct, context)?;
817                values
818                    .into_iter()
819                    .min_by(|a, b| self.compare_algebra_terms(a, b))
820                    .ok_or_else(|| anyhow!("No values for MIN"))
821            }
822            Aggregate::Max { expr, distinct } => {
823                let values =
824                    self.collect_aggregate_values(Some(expr), group, *distinct, context)?;
825                values
826                    .into_iter()
827                    .max_by(|a, b| self.compare_algebra_terms(a, b))
828                    .ok_or_else(|| anyhow!("No values for MAX"))
829            }
830            Aggregate::Avg { expr, distinct } => {
831                let values =
832                    self.collect_aggregate_values(Some(expr), group, *distinct, context)?;
833                let sum = self.sum_numeric_values(values.clone())?;
834                let count = values.len() as f64;
835
836                match sum {
837                    AlgebraTerm::Literal(lit) => {
838                        let val = lit.value.parse::<f64>().unwrap_or(0.0);
839                        Ok(AlgebraTerm::Literal(Literal::typed(
840                            (val / count).to_string(),
841                            NamedNode::new_unchecked("http://www.w3.org/2001/XMLSchema#decimal"),
842                        )))
843                    }
844                    _ => Err(anyhow!("Invalid sum for AVG")),
845                }
846            }
847            Aggregate::GroupConcat {
848                expr,
849                separator,
850                distinct,
851            } => {
852                let values =
853                    self.collect_aggregate_values(Some(expr), group, *distinct, context)?;
854                let sep = separator.as_deref().unwrap_or(" ");
855                let concat = values
856                    .iter()
857                    .map(|v| self.term_to_string(v))
858                    .collect::<Vec<_>>()
859                    .join(sep);
860                Ok(AlgebraTerm::Literal(Literal::string(concat)))
861            }
862            _ => Err(anyhow!("Unsupported aggregate")),
863        }
864    }
865
866    /// Collect values for aggregation
867    fn collect_aggregate_values(
868        &self,
869        expr: Option<&Expression>,
870        group: &[Binding],
871        distinct: bool,
872        context: &ExecutionContext,
873    ) -> Result<Vec<AlgebraTerm>> {
874        let extension_registry = context.extension_registry.clone();
875
876        let mut values: Vec<AlgebraTerm> = group
877            .par_iter()
878            .filter_map(|binding| {
879                if let Some(expr) = expr {
880                    let mut ctx = BindingContext::new();
881                    for (var, term) in binding {
882                        ctx.bind(var.as_str(), Term::from_algebra_term(term));
883                    }
884                    let evaluator =
885                        ExpressionEvaluator::with_context(extension_registry.clone(), ctx);
886                    evaluator.evaluate(expr).ok().map(|t| t.to_algebra_term())
887                } else {
888                    // COUNT(*) case
889                    Some(AlgebraTerm::Literal(Literal::string("1")))
890                }
891            })
892            .collect();
893
894        if distinct {
895            values.sort();
896            values.dedup();
897        }
898
899        Ok(values)
900    }
901
902    /// Sum numeric values
903    fn sum_numeric_values(&self, values: Vec<AlgebraTerm>) -> Result<AlgebraTerm> {
904        let sum = values
905            .into_par_iter()
906            .filter_map(|term| {
907                if let AlgebraTerm::Literal(lit) = term {
908                    lit.value.parse::<f64>().ok()
909                } else {
910                    None
911                }
912            })
913            .sum::<f64>();
914
915        Ok(AlgebraTerm::Literal(Literal::typed(
916            sum.to_string(),
917            NamedNode::new_unchecked("http://www.w3.org/2001/XMLSchema#decimal"),
918        )))
919    }
920
921    /// Convert term to string
922    fn term_to_string(&self, term: &AlgebraTerm) -> String {
923        match term {
924            AlgebraTerm::Iri(iri) => iri.as_str().to_string(),
925            AlgebraTerm::Literal(lit) => lit.value.to_string(),
926            AlgebraTerm::Variable(var) => format!("?{var}"),
927            AlgebraTerm::BlankNode(id) => format!("_:{id}"),
928            AlgebraTerm::QuotedTriple(_) => "<<quoted triple>>".to_string(),
929            AlgebraTerm::PropertyPath(_) => "<<property path>>".to_string(),
930        }
931    }
932
933    /// Calculate thread utilization
934    fn calculate_thread_utilization(&self) -> f64 {
935        // Simplified calculation - in practice would track actual thread usage
936        self.thread_pool.current_num_threads() as f64 / self.config.max_threads as f64
937    }
938
939    /// Get parallel execution statistics
940    pub fn get_stats(&self) -> ParallelStats {
941        let stats = self.stats.read();
942        ParallelStats {
943            parallel_operations: stats.parallel_operations,
944            work_items_processed: stats.work_items_processed,
945            thread_utilization: stats.thread_utilization,
946            parallel_speedup: stats.parallel_speedup,
947            cache_hits: stats.cache_hits,
948            cache_misses: stats.cache_misses,
949        }
950    }
951}