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    /// # Returns
54    ///
55    /// GraphQL-compatible JSON array of objects
56    ///
57    /// # Example
58    ///
59    /// ```rust,ignore
60    /// let rows = vec![
61    ///     hashmap!{
62    ///         "revenue" => json!(100.00),
63    ///         "category" => json!("Electronics"),
64    ///         "rank" => json!(1)
65    ///     }
66    /// ];
67    ///
68    /// let result = WindowProjector::project(rows, &plan)?;
69    /// // result: [{"revenue": 100.00, "category": "Electronics", "rank": 1}]
70    /// ```
71    pub fn project(
72        rows: Vec<HashMap<String, Value>>,
73        _plan: &WindowExecutionPlan,
74    ) -> Result<Value> {
75        // Simple projection: convert each row HashMap to JSON object
76        // Future enhancements could include:
77        // - Type coercion (ensure numbers are numbers, not strings)
78        // - Null handling
79        // - Alias mapping (SQL alias → GraphQL field name)
80        // - Decimal precision handling
81
82        let projected_rows: Vec<Value> = rows
83            .into_iter()
84            .map(|row| {
85                let mut obj = serde_json::Map::new();
86                for (key, value) in row {
87                    obj.insert(key, value);
88                }
89                Value::Object(obj)
90            })
91            .collect();
92
93        Ok(Value::Array(projected_rows))
94    }
95
96    /// Wrap projected results in a GraphQL data envelope.
97    ///
98    /// # Arguments
99    ///
100    /// * `projected` - The projected JSON value (array of objects)
101    /// * `query_name` - The GraphQL field name (e.g., "sales_window")
102    ///
103    /// # Returns
104    ///
105    /// Complete GraphQL response structure
106    ///
107    /// # Example
108    ///
109    /// ```rust,ignore
110    /// let projected = json!([{"rank": 1}, {"rank": 2}]);
111    /// let response = WindowProjector::wrap_in_data_envelope(projected, "sales_window");
112    /// // { "data": { "sales_window": [{"rank": 1}, {"rank": 2}] } }
113    /// ```
114    #[must_use]
115    pub fn wrap_in_data_envelope(projected: Value, query_name: &str) -> Value {
116        let mut data = serde_json::Map::new();
117        data.insert(query_name.to_string(), projected);
118
119        let mut response = serde_json::Map::new();
120        response.insert("data".to_string(), Value::Object(data));
121
122        Value::Object(response)
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use serde_json::json;
129
130    use super::*;
131    use crate::compiler::window_functions::{
132        SelectColumn, WindowExecutionPlan, WindowFunction, WindowFunctionType,
133    };
134
135    fn create_test_plan() -> WindowExecutionPlan {
136        WindowExecutionPlan {
137            table:        "tf_sales".to_string(),
138            select:       vec![
139                SelectColumn {
140                    expression: "revenue".to_string(),
141                    alias:      "revenue".to_string(),
142                },
143                SelectColumn {
144                    expression: "category".to_string(),
145                    alias:      "category".to_string(),
146                },
147            ],
148            windows:      vec![WindowFunction {
149                function:     WindowFunctionType::RowNumber,
150                alias:        "rank".to_string(),
151                partition_by: vec!["category".to_string()],
152                order_by:     vec![],
153                frame:        None,
154            }],
155            where_clause: None,
156            order_by:     vec![],
157            limit:        None,
158            offset:       None,
159        }
160    }
161
162    #[test]
163    fn test_project_empty_results() {
164        let plan = create_test_plan();
165        let rows: Vec<HashMap<String, Value>> = vec![];
166
167        let result = WindowProjector::project(rows, &plan).unwrap();
168        assert_eq!(result, json!([]));
169    }
170
171    #[test]
172    fn test_project_single_row() {
173        let plan = create_test_plan();
174        let mut row = HashMap::new();
175        row.insert("revenue".to_string(), json!(100.00));
176        row.insert("category".to_string(), json!("Electronics"));
177        row.insert("rank".to_string(), json!(1));
178
179        let rows = vec![row];
180        let result = WindowProjector::project(rows, &plan).unwrap();
181
182        let expected = json!([
183            {"revenue": 100.00, "category": "Electronics", "rank": 1}
184        ]);
185        assert_eq!(result, expected);
186    }
187
188    #[test]
189    fn test_project_multiple_rows() {
190        let plan = create_test_plan();
191
192        let mut row1 = HashMap::new();
193        row1.insert("revenue".to_string(), json!(100.00));
194        row1.insert("category".to_string(), json!("Electronics"));
195        row1.insert("rank".to_string(), json!(1));
196
197        let mut row2 = HashMap::new();
198        row2.insert("revenue".to_string(), json!(150.00));
199        row2.insert("category".to_string(), json!("Electronics"));
200        row2.insert("rank".to_string(), json!(2));
201
202        let mut row3 = HashMap::new();
203        row3.insert("revenue".to_string(), json!(50.00));
204        row3.insert("category".to_string(), json!("Books"));
205        row3.insert("rank".to_string(), json!(1));
206
207        let rows = vec![row1, row2, row3];
208        let result = WindowProjector::project(rows, &plan).unwrap();
209
210        let expected = json!([
211            {"revenue": 100.00, "category": "Electronics", "rank": 1},
212            {"revenue": 150.00, "category": "Electronics", "rank": 2},
213            {"revenue": 50.00, "category": "Books", "rank": 1}
214        ]);
215        assert_eq!(result, expected);
216    }
217
218    #[test]
219    fn test_wrap_in_data_envelope() {
220        let projected = json!([{"rank": 1}, {"rank": 2}]);
221        let response = WindowProjector::wrap_in_data_envelope(projected, "sales_window");
222
223        let expected = json!({
224            "data": {
225                "sales_window": [{"rank": 1}, {"rank": 2}]
226            }
227        });
228        assert_eq!(response, expected);
229    }
230
231    #[test]
232    fn test_project_with_null_values() {
233        let plan = create_test_plan();
234
235        let mut row = HashMap::new();
236        row.insert("revenue".to_string(), json!(null));
237        row.insert("category".to_string(), json!("Unknown"));
238        row.insert("rank".to_string(), json!(1));
239
240        let rows = vec![row];
241        let result = WindowProjector::project(rows, &plan).unwrap();
242
243        let expected = json!([
244            {"revenue": null, "category": "Unknown", "rank": 1}
245        ]);
246        assert_eq!(result, expected);
247    }
248
249    #[test]
250    fn test_project_with_numeric_types() {
251        let plan = create_test_plan();
252
253        let mut row = HashMap::new();
254        row.insert("revenue".to_string(), json!(1234.56));
255        row.insert("category".to_string(), json!("Electronics"));
256        row.insert("rank".to_string(), json!(1));
257        row.insert("running_total".to_string(), json!(5000.00));
258        row.insert("row_count".to_string(), json!(42));
259
260        let rows = vec![row];
261        let result = WindowProjector::project(rows, &plan).unwrap();
262
263        // Verify numeric values are preserved
264        let arr = result.as_array().unwrap();
265        let first_row = &arr[0];
266        assert_eq!(first_row["revenue"], json!(1234.56));
267        assert_eq!(first_row["rank"], json!(1));
268        assert_eq!(first_row["running_total"], json!(5000.00));
269        assert_eq!(first_row["row_count"], json!(42));
270    }
271}