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