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}