velesdb_core/database/query_engine_agg.rs
1//! Database-level aggregation entry point.
2//!
3//! Extracted from `query_engine.rs` (file NLOC budget) so every surface — the
4//! server `/query` handler, the CLI REPL, and future SDK consumers — can route
5//! `GROUP BY` / scalar-aggregate queries through one method instead of each
6//! re-implementing collection resolution.
7
8use std::collections::HashMap;
9
10use super::Database;
11use crate::{Error, Result};
12
13impl Database {
14 /// Executes a `GROUP BY` / scalar-aggregate SELECT, returning the aggregate
15 /// result as JSON (a single object for scalar aggregates, an array of group
16 /// objects for `GROUP BY`).
17 ///
18 /// The target collection is resolved from the query's `FROM` clause, falling
19 /// back to a `"_collection"` key in `params` (the convention the REPL/SDK use
20 /// to inject the active collection). Callers should gate on
21 /// [`crate::velesql::SelectStatement::is_aggregation_query`] — non-aggregate
22 /// SELECTs belong on [`Database::execute_query`].
23 ///
24 /// # Errors
25 ///
26 /// Returns an error if validation fails, the collection cannot be resolved,
27 /// or aggregation execution fails.
28 pub fn execute_aggregate(
29 &self,
30 query: &crate::velesql::Query,
31 params: &HashMap<String, serde_json::Value>,
32 ) -> Result<serde_json::Value> {
33 // Resolve scalar subqueries (EPIC-039) in WHERE/HAVING before validation
34 // so the aggregate engine sees a subquery-free AST.
35 if let Some(rewritten) = self.resolve_subqueries(query, params)? {
36 return self.execute_aggregate(&rewritten, params);
37 }
38 crate::velesql::QueryValidator::validate(query).map_err(|e| Error::Query(e.to_string()))?;
39 let name = aggregate_target_collection(query, params)?;
40 let collection = self
41 .get_any_collection(&name)
42 .ok_or(Error::CollectionNotFound(name))?;
43 collection.execute_aggregate(query, params)
44 }
45}
46
47/// Resolves the target collection name for an aggregation query: the `FROM`
48/// clause, else a `"_collection"` param, else a guidance error.
49fn aggregate_target_collection(
50 query: &crate::velesql::Query,
51 params: &HashMap<String, serde_json::Value>,
52) -> Result<String> {
53 if !query.select.from.is_empty() {
54 return Ok(query.select.from.clone());
55 }
56 if let Some(serde_json::Value::String(name)) = params.get("_collection") {
57 return Ok(name.clone());
58 }
59 Err(Error::Query(
60 "aggregation query requires a target collection. Use SELECT ... FROM \
61 <collection> ... GROUP BY, or pass {\"_collection\": \"name\"} in params."
62 .to_string(),
63 ))
64}