Skip to main content

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}