Skip to main content

fraiseql_core/runtime/
window_projector.rs

1//! Window Function Result Projector
2//!
3//! Projects SQL window function results to GraphQL JSON.
4//!
5//! # Overview
6//!
7//! Window functions return rows with computed window values alongside regular columns.
8//! This module transforms raw SQL results into GraphQL-compatible JSON format.
9//!
10//! # Example
11//!
12//! SQL Result:
13//! ```text
14//! | revenue | category    | rank | running_total |
15//! |---------|-------------|------|---------------|
16//! | 100.00  | Electronics | 1    | 100.00        |
17//! | 150.00  | Electronics | 2    | 250.00        |
18//! | 50.00   | Books       | 1    | 50.00         |
19//! ```
20//!
21//! GraphQL Response:
22//! ```json
23//! {
24//!   "data": {
25//!     "sales_window": [
26//!       {"revenue": 100.00, "category": "Electronics", "rank": 1, "running_total": 100.00},
27//!       {"revenue": 150.00, "category": "Electronics", "rank": 2, "running_total": 250.00},
28//!       {"revenue": 50.00, "category": "Books", "rank": 1, "running_total": 50.00}
29//!     ]
30//!   }
31//! }
32//! ```
33
34use std::collections::HashMap;
35
36use serde_json::Value;
37
38use crate::{compiler::window_functions::WindowExecutionPlan, error::Result};
39
40/// Window function result projector.
41///
42/// Transforms SQL query results into GraphQL-compatible JSON format.
43pub struct WindowProjector;
44
45impl WindowProjector {
46    /// Project SQL window function results to GraphQL JSON.
47    ///
48    /// # Arguments
49    ///
50    /// * `rows` - SQL result rows as `HashMaps` (column name → value)
51    /// * `plan` - Window execution plan (for metadata like aliases)
52    ///
53    /// # Errors
54    ///
55    /// Currently infallible; reserved for future extension (e.g., type coercion failures).
56    ///
57    /// # Returns
58    ///
59    /// GraphQL-compatible JSON array of objects
60    ///
61    /// # Example
62    ///
63    /// ```no_run
64    /// // Requires: a WindowExecutionPlan built from compiled schema metadata.
65    /// // See: tests/integration/ for runnable examples.
66    /// use std::collections::HashMap;
67    /// use serde_json::json;
68    /// # use fraiseql_core::runtime::WindowProjector;
69    ///
70    /// let mut row = HashMap::new();
71    /// row.insert("revenue".to_string(), json!(100.00));
72    /// row.insert("category".to_string(), json!("Electronics"));
73    /// row.insert("rank".to_string(), json!(1));
74    /// let rows = vec![row];
75    /// // let result = WindowProjector::project(rows, &plan)?;
76    /// // result: [{"revenue": 100.00, "category": "Electronics", "rank": 1}]
77    /// ```
78    pub fn project(
79        rows: Vec<HashMap<String, Value>>,
80        _plan: &WindowExecutionPlan,
81    ) -> Result<Value> {
82        // Simple projection: convert each row HashMap to JSON object
83        // Future enhancements could include:
84        // - Type coercion (ensure numbers are numbers, not strings)
85        // - Null handling
86        // - Alias mapping (SQL alias → GraphQL field name)
87        // - Decimal precision handling
88
89        let projected_rows: Vec<Value> = rows
90            .into_iter()
91            .map(|row| {
92                let mut obj = serde_json::Map::new();
93                for (key, value) in row {
94                    obj.insert(key, value);
95                }
96                Value::Object(obj)
97            })
98            .collect();
99
100        Ok(Value::Array(projected_rows))
101    }
102
103    /// Wrap projected results in a GraphQL data envelope.
104    ///
105    /// # Arguments
106    ///
107    /// * `projected` - The projected JSON value (array of objects)
108    /// * `query_name` - The GraphQL field name (e.g., "`sales_window`")
109    ///
110    /// # Returns
111    ///
112    /// Complete GraphQL response structure
113    ///
114    /// # Example
115    ///
116    /// ```rust
117    /// # use fraiseql_core::runtime::WindowProjector;
118    /// # use serde_json::json;
119    /// let projected = json!([{"rank": 1}, {"rank": 2}]);
120    /// let response = WindowProjector::wrap_in_data_envelope(projected, "sales_window");
121    /// // { "data": { "sales_window": [{"rank": 1}, {"rank": 2}] } }
122    /// assert!(response.get("data").is_some());
123    /// ```
124    #[must_use]
125    pub fn wrap_in_data_envelope(projected: Value, query_name: &str) -> Value {
126        let mut data = serde_json::Map::new();
127        data.insert(query_name.to_string(), projected);
128
129        let mut response = serde_json::Map::new();
130        response.insert("data".to_string(), Value::Object(data));
131
132        Value::Object(response)
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    #![allow(clippy::unwrap_used)] // Reason: test code, panics are acceptable
139
140    use serde_json::json;
141
142    use super::*;
143    use crate::compiler::window_functions::{
144        SelectColumn, WindowExecutionPlan, WindowFunction, WindowFunctionType,
145    };
146
147    fn create_test_plan() -> WindowExecutionPlan {
148        WindowExecutionPlan {
149            table:        "tf_sales".to_string(),
150            select:       vec![
151                SelectColumn {
152                    expression: "revenue".to_string(),
153                    alias:      "revenue".to_string(),
154                },
155                SelectColumn {
156                    expression: "category".to_string(),
157                    alias:      "category".to_string(),
158                },
159            ],
160            windows:      vec![WindowFunction {
161                function:     WindowFunctionType::RowNumber,
162                alias:        "rank".to_string(),
163                partition_by: vec!["category".to_string()],
164                order_by:     vec![],
165                frame:        None,
166            }],
167            where_clause: None,
168            order_by:     vec![],
169            limit:        None,
170            offset:       None,
171        }
172    }
173
174    #[test]
175    fn test_project_empty_results() {
176        let plan = create_test_plan();
177        let rows: Vec<HashMap<String, Value>> = vec![];
178
179        let result = WindowProjector::project(rows, &plan).unwrap();
180        assert_eq!(result, json!([]));
181    }
182
183    #[test]
184    fn test_project_single_row() {
185        let plan = create_test_plan();
186        let mut row = HashMap::new();
187        row.insert("revenue".to_string(), json!(100.00));
188        row.insert("category".to_string(), json!("Electronics"));
189        row.insert("rank".to_string(), json!(1));
190
191        let rows = vec![row];
192        let result = WindowProjector::project(rows, &plan).unwrap();
193
194        let expected = json!([
195            {"revenue": 100.00, "category": "Electronics", "rank": 1}
196        ]);
197        assert_eq!(result, expected);
198    }
199
200    #[test]
201    fn test_project_multiple_rows() {
202        let plan = create_test_plan();
203
204        let mut row1 = HashMap::new();
205        row1.insert("revenue".to_string(), json!(100.00));
206        row1.insert("category".to_string(), json!("Electronics"));
207        row1.insert("rank".to_string(), json!(1));
208
209        let mut row2 = HashMap::new();
210        row2.insert("revenue".to_string(), json!(150.00));
211        row2.insert("category".to_string(), json!("Electronics"));
212        row2.insert("rank".to_string(), json!(2));
213
214        let mut row3 = HashMap::new();
215        row3.insert("revenue".to_string(), json!(50.00));
216        row3.insert("category".to_string(), json!("Books"));
217        row3.insert("rank".to_string(), json!(1));
218
219        let rows = vec![row1, row2, row3];
220        let result = WindowProjector::project(rows, &plan).unwrap();
221
222        let expected = json!([
223            {"revenue": 100.00, "category": "Electronics", "rank": 1},
224            {"revenue": 150.00, "category": "Electronics", "rank": 2},
225            {"revenue": 50.00, "category": "Books", "rank": 1}
226        ]);
227        assert_eq!(result, expected);
228    }
229
230    #[test]
231    fn test_wrap_in_data_envelope() {
232        let projected = json!([{"rank": 1}, {"rank": 2}]);
233        let response = WindowProjector::wrap_in_data_envelope(projected, "sales_window");
234
235        let expected = json!({
236            "data": {
237                "sales_window": [{"rank": 1}, {"rank": 2}]
238            }
239        });
240        assert_eq!(response, expected);
241    }
242
243    #[test]
244    fn test_project_with_null_values() {
245        let plan = create_test_plan();
246
247        let mut row = HashMap::new();
248        row.insert("revenue".to_string(), json!(null));
249        row.insert("category".to_string(), json!("Unknown"));
250        row.insert("rank".to_string(), json!(1));
251
252        let rows = vec![row];
253        let result = WindowProjector::project(rows, &plan).unwrap();
254
255        let expected = json!([
256            {"revenue": null, "category": "Unknown", "rank": 1}
257        ]);
258        assert_eq!(result, expected);
259    }
260
261    #[test]
262    fn test_project_with_numeric_types() {
263        let plan = create_test_plan();
264
265        let mut row = HashMap::new();
266        row.insert("revenue".to_string(), json!(1234.56));
267        row.insert("category".to_string(), json!("Electronics"));
268        row.insert("rank".to_string(), json!(1));
269        row.insert("running_total".to_string(), json!(5000.00));
270        row.insert("row_count".to_string(), json!(42));
271
272        let rows = vec![row];
273        let result = WindowProjector::project(rows, &plan).unwrap();
274
275        // Verify numeric values are preserved
276        let arr = result.as_array().unwrap();
277        let first_row = &arr[0];
278        assert_eq!(first_row["revenue"], json!(1234.56));
279        assert_eq!(first_row["rank"], json!(1));
280        assert_eq!(first_row["running_total"], json!(5000.00));
281        assert_eq!(first_row["row_count"], json!(42));
282    }
283}