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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
//! Aggregation Result Projector
//!
//! Projects SQL aggregate results to GraphQL JSON responses.
//!
//! # SQL Result Format
//!
//! SQL returns rows as `Vec<HashMap<String, Value>>`:
//! ```json
//! [
//! {
//! "category": "Electronics",
//! "occurred_at_day": "2025-01-01T00:00:00Z",
//! "count": 42,
//! "revenue_sum": 5280.50,
//! "revenue_avg": 125.73
//! }
//! ]
//! ```
//!
//! # GraphQL Response Format
//!
//! Projected to GraphQL response:
//! ```json
//! {
//! "data": {
//! "sales_aggregate": [
//! {
//! "category": "Electronics",
//! "occurred_at_day": "2025-01-01T00:00:00Z",
//! "count": 42,
//! "revenue_sum": 5280.50,
//! "revenue_avg": 125.73
//! }
//! ]
//! }
//! }
//! ```
use std::collections::HashMap;
use serde_json::{Value, json};
#[allow(unused_imports)] // Reason: used only in doc links for `# Errors` sections
use crate::error::FraiseQLError;
use crate::{compiler::aggregation::AggregationPlan, error::Result};
/// Aggregation result projector
pub struct AggregationProjector;
impl AggregationProjector {
/// Project SQL aggregate results to GraphQL JSON.
///
/// # Arguments
///
/// * `rows` - SQL result rows as `HashMaps`
/// * `plan` - Aggregation execution plan (for metadata)
///
/// # Returns
///
/// GraphQL-compatible JSON response
///
/// # Errors
///
/// Currently infallible; reserved for future extension (e.g., type coercion failures).
///
/// # Example
///
/// ```no_run
/// // Requires: an AggregationPlan built from compiled schema metadata.
/// // See: tests/integration/ for runnable examples.
/// use std::collections::HashMap;
/// use serde_json::{json, Value};
/// # use fraiseql_core::runtime::AggregationProjector;
///
/// let mut row = HashMap::new();
/// row.insert("category".to_string(), json!("Electronics"));
/// row.insert("count".to_string(), json!(42));
/// row.insert("revenue_sum".to_string(), json!(5280.50));
/// let rows = vec![row];
/// // let result = AggregationProjector::project(rows, &plan)?;
/// // result: [{"category": "Electronics", "count": 42, "revenue_sum": 5280.50}]
/// ```
///
/// # Errors
///
/// Returns [`FraiseQLError::Internal`] if JSON serialization of the projected
/// rows fails (should not occur for well-formed input).
pub fn project(rows: Vec<HashMap<String, Value>>, _plan: &AggregationPlan) -> Result<Value> {
// For simple projection: just convert rows to JSON array
// Future improvements could include:
// - Type coercion (ensure numbers are numbers, not strings)
// - Null handling
// - Nested object construction
// - Date formatting
let projected_rows: Vec<Value> = rows
.into_iter()
.map(|row| {
// Convert HashMap to JSON object
let mut obj = serde_json::Map::new();
for (key, value) in row {
obj.insert(key, value);
}
Value::Object(obj)
})
.collect();
Ok(Value::Array(projected_rows))
}
/// Wrap projected results in GraphQL data envelope.
///
/// # Arguments
///
/// * `projected` - Projected result array
/// * `query_name` - GraphQL query field name (e.g., "`sales_aggregate`")
///
/// # Returns
///
/// Complete GraphQL response with `{"data": {...}}` wrapper
///
/// # Example
///
/// ```rust
/// # use fraiseql_core::runtime::AggregationProjector;
/// # use serde_json::json;
/// let projected = json!([{"count": 42}]);
/// let response = AggregationProjector::wrap_in_data_envelope(projected, "sales_aggregate");
/// // response: {"data": {"sales_aggregate": [{"count": 42}]}}
/// assert!(response.get("data").is_some());
/// ```
#[allow(clippy::needless_pass_by_value)] // Reason: projected is moved into serde_json::json! and consumed by value
#[must_use]
pub fn wrap_in_data_envelope(projected: Value, query_name: &str) -> Value {
json!({
"data": {
query_name: projected
}
})
}
/// Project a single aggregate result (no GROUP BY).
///
/// When there's no GROUP BY, the result is a single object, not an array.
///
/// # Errors
///
/// Currently infallible; reserved for future extension (e.g., type coercion failures).
///
/// # Example
///
/// ```no_run
/// // Requires: an AggregationPlan built from compiled schema metadata.
/// // See: tests/integration/ for runnable examples.
/// use std::collections::HashMap;
/// use serde_json::json;
/// # use fraiseql_core::runtime::AggregationProjector;
///
/// let mut row = HashMap::new();
/// row.insert("count".to_string(), json!(100));
/// row.insert("revenue_sum".to_string(), json!(5000.0));
/// // let result = AggregationProjector::project_single(row, &plan)?;
/// // result: {"count": 100, "revenue_sum": 5000.0}
/// ```
pub fn project_single(row: HashMap<String, Value>, _plan: &AggregationPlan) -> Result<Value> {
// Convert HashMap to JSON object
let mut obj = serde_json::Map::new();
for (key, value) in row {
obj.insert(key, value);
}
Ok(Value::Object(obj))
}
}