Skip to main content

fraiseql_core/runtime/
window_parser.rs

1//! Window Query Parser
2//!
3//! Parses GraphQL window queries into `WindowRequest` for execution.
4//!
5//! # GraphQL Query Format
6//!
7//! ```graphql
8//! query {
9//!   sales_window(
10//!     where: { customer_id_eq: "uuid-123" }
11//!     orderBy: { occurred_at: ASC }
12//!     limit: 100
13//!   ) {
14//!     revenue
15//!     category
16//!     rank: row_number(partitionBy: ["category"], orderBy: { revenue: DESC })
17//!     running_total: sum(field: "revenue", orderBy: { occurred_at: ASC })
18//!     prev_revenue: lag(field: "revenue", offset: 1, default: 0)
19//!   }
20//! }
21//! ```
22//!
23//! # JSON Query Format
24//!
25//! ```json
26//! {
27//!   "table": "tf_sales",
28//!   "select": [
29//!     {"type": "measure", "name": "revenue", "alias": "revenue"},
30//!     {"type": "dimension", "path": "category", "alias": "category"}
31//!   ],
32//!   "windows": [
33//!     {
34//!       "function": {"type": "row_number"},
35//!       "alias": "rank",
36//!       "partitionBy": [{"type": "dimension", "path": "category"}],
37//!       "orderBy": [{"field": "revenue", "direction": "DESC"}]
38//!     },
39//!     {
40//!       "function": {"type": "running_sum", "measure": "revenue"},
41//!       "alias": "running_total",
42//!       "orderBy": [{"field": "occurred_at", "direction": "ASC"}],
43//!       "frame": {"frame_type": "ROWS", "start": {"type": "unbounded_preceding"}, "end": {"type": "current_row"}}
44//!     }
45//!   ],
46//!   "orderBy": [{"field": "occurred_at", "direction": "ASC"}],
47//!   "limit": 100
48//! }
49//! ```
50
51use serde_json::Value;
52
53use super::aggregate_parser::split_where_key;
54use crate::{
55    backend::where_clause::WhereClause,
56    compiler::{
57        aggregation::OrderDirection,
58        fact_table::FactTableMetadata,
59        window_functions::{
60            FrameBoundary, FrameExclusion, FrameType, PartitionByColumn, WindowFrame,
61            WindowFunctionRequest, WindowFunctionSpec, WindowOrderBy, WindowRequest,
62            WindowSelectColumn,
63        },
64    },
65    error::{FraiseQLError, Result},
66};
67
68/// Window query parser
69pub struct WindowQueryParser;
70
71impl WindowQueryParser {
72    /// Parse a window query JSON into `WindowRequest`.
73    ///
74    /// # Arguments
75    ///
76    /// * `query_json` - JSON representation of the window query
77    /// * `metadata` - Fact table metadata: its declared fields settle an ambiguous `where` key
78    ///
79    /// # Errors
80    ///
81    /// Returns error if the query structure is invalid.
82    pub fn parse(query_json: &Value, metadata: &FactTableMetadata) -> Result<WindowRequest> {
83        // Extract table name
84        let table_name = query_json
85            .get("table")
86            .and_then(|v| v.as_str())
87            .ok_or_else(|| FraiseQLError::Validation {
88                message: "Missing 'table' field in window query".to_string(),
89                path:    None,
90            })?
91            .to_string();
92
93        // Parse SELECT columns
94        let select = if let Some(select_array) = query_json.get("select") {
95            Self::parse_select_columns(select_array)?
96        } else {
97            vec![]
98        };
99
100        // Parse window functions
101        let windows = if let Some(windows_array) = query_json.get("windows") {
102            Self::parse_window_functions(windows_array)?
103        } else {
104            vec![]
105        };
106
107        // Parse WHERE clause
108        let where_clause = if let Some(where_obj) = query_json.get("where") {
109            Some(Self::parse_where_clause(where_obj, metadata)?)
110        } else {
111            None
112        };
113
114        // Parse final ORDER BY
115        let order_by = if let Some(order_array) = query_json.get("orderBy") {
116            Self::parse_order_by(order_array)?
117        } else {
118            vec![]
119        };
120
121        // Parse LIMIT/OFFSET
122        let limit = query_json
123            .get("limit")
124            .and_then(|v| v.as_u64())
125            .map(|n| u32::try_from(n).unwrap_or(u32::MAX));
126
127        let offset = query_json
128            .get("offset")
129            .and_then(|v| v.as_u64())
130            .map(|n| u32::try_from(n).unwrap_or(u32::MAX));
131
132        Ok(WindowRequest {
133            table_name,
134            select,
135            windows,
136            where_clause,
137            order_by,
138            limit,
139            offset,
140        })
141    }
142
143    /// Parse SELECT columns from JSON array.
144    fn parse_select_columns(select_array: &Value) -> Result<Vec<WindowSelectColumn>> {
145        let Some(arr) = select_array.as_array() else {
146            return Ok(vec![]);
147        };
148
149        arr.iter().map(Self::parse_single_select_column).collect()
150    }
151
152    fn parse_single_select_column(col: &Value) -> Result<WindowSelectColumn> {
153        let col_type =
154            col.get("type")
155                .and_then(|v| v.as_str())
156                .ok_or_else(|| FraiseQLError::Validation {
157                    message: "Missing 'type' in select column".to_string(),
158                    path:    None,
159                })?;
160
161        let alias = col
162            .get("alias")
163            .and_then(|v| v.as_str())
164            .ok_or_else(|| FraiseQLError::Validation {
165                message: "Missing 'alias' in select column".to_string(),
166                path:    None,
167            })?
168            .to_string();
169
170        match col_type {
171            "measure" => {
172                let name = col
173                    .get("name")
174                    .and_then(|v| v.as_str())
175                    .ok_or_else(|| FraiseQLError::Validation {
176                        message: "Missing 'name' in measure select column".to_string(),
177                        path:    None,
178                    })?
179                    .to_string();
180                Ok(WindowSelectColumn::Measure { name, alias })
181            },
182            "dimension" => {
183                let path = col
184                    .get("path")
185                    .and_then(|v| v.as_str())
186                    .ok_or_else(|| FraiseQLError::Validation {
187                        message: "Missing 'path' in dimension select column".to_string(),
188                        path:    None,
189                    })?
190                    .to_string();
191                Ok(WindowSelectColumn::Dimension { path, alias })
192            },
193            "filter" => {
194                let name = col
195                    .get("name")
196                    .and_then(|v| v.as_str())
197                    .ok_or_else(|| FraiseQLError::Validation {
198                        message: "Missing 'name' in filter select column".to_string(),
199                        path:    None,
200                    })?
201                    .to_string();
202                Ok(WindowSelectColumn::Filter { name, alias })
203            },
204            _ => Err(FraiseQLError::Validation {
205                message: format!("Unknown select column type: {col_type}"),
206                path:    None,
207            }),
208        }
209    }
210
211    /// Parse window functions from JSON array.
212    fn parse_window_functions(windows_array: &Value) -> Result<Vec<WindowFunctionRequest>> {
213        let Some(arr) = windows_array.as_array() else {
214            return Ok(vec![]);
215        };
216
217        arr.iter().map(Self::parse_single_window_function).collect()
218    }
219
220    fn parse_single_window_function(window: &Value) -> Result<WindowFunctionRequest> {
221        // Parse function spec
222        let function = window
223            .get("function")
224            .ok_or_else(|| FraiseQLError::Validation {
225                message: "Missing 'function' in window definition".to_string(),
226                path:    None,
227            })
228            .and_then(Self::parse_function_spec)?;
229
230        // Parse alias
231        let alias = window
232            .get("alias")
233            .and_then(|v| v.as_str())
234            .ok_or_else(|| FraiseQLError::Validation {
235                message: "Missing 'alias' in window definition".to_string(),
236                path:    None,
237            })?
238            .to_string();
239
240        // Parse PARTITION BY
241        let partition_by = if let Some(partition_array) = window.get("partitionBy") {
242            Self::parse_partition_by(partition_array)?
243        } else {
244            vec![]
245        };
246
247        // Parse ORDER BY within window
248        let order_by = if let Some(order_array) = window.get("orderBy") {
249            Self::parse_order_by(order_array)?
250        } else {
251            vec![]
252        };
253
254        // Parse frame
255        let frame = window.get("frame").map(Self::parse_frame).transpose()?;
256
257        Ok(WindowFunctionRequest {
258            function,
259            alias,
260            partition_by,
261            order_by,
262            frame,
263        })
264    }
265
266    /// Parse window function specification.
267    fn parse_function_spec(func: &Value) -> Result<WindowFunctionSpec> {
268        let func_type =
269            func.get("type")
270                .and_then(|v| v.as_str())
271                .ok_or_else(|| FraiseQLError::Validation {
272                    message: "Missing 'type' in function spec".to_string(),
273                    path:    None,
274                })?;
275
276        match func_type {
277            // Ranking functions
278            "row_number" => Ok(WindowFunctionSpec::RowNumber),
279            "rank" => Ok(WindowFunctionSpec::Rank),
280            "dense_rank" => Ok(WindowFunctionSpec::DenseRank),
281            "ntile" => {
282                let n = u32::try_from(func.get("n").and_then(|v| v.as_u64()).ok_or_else(|| {
283                    FraiseQLError::Validation {
284                        message: "Missing 'n' in NTILE function".to_string(),
285                        path:    None,
286                    }
287                })?)
288                .unwrap_or(u32::MAX);
289                Ok(WindowFunctionSpec::Ntile { n })
290            },
291            "percent_rank" => Ok(WindowFunctionSpec::PercentRank),
292            "cume_dist" => Ok(WindowFunctionSpec::CumeDist),
293
294            // Value functions
295            "lag" => {
296                let field = Self::extract_string_field(func, "field")?;
297                let offset =
298                    i32::try_from(func.get("offset").and_then(|v| v.as_i64()).unwrap_or(1))
299                        .unwrap_or(1);
300                let default = func.get("default").cloned();
301                Ok(WindowFunctionSpec::Lag {
302                    field,
303                    offset,
304                    default,
305                })
306            },
307            "lead" => {
308                let field = Self::extract_string_field(func, "field")?;
309                let offset =
310                    i32::try_from(func.get("offset").and_then(|v| v.as_i64()).unwrap_or(1))
311                        .unwrap_or(1);
312                let default = func.get("default").cloned();
313                Ok(WindowFunctionSpec::Lead {
314                    field,
315                    offset,
316                    default,
317                })
318            },
319            "first_value" => {
320                let field = Self::extract_string_field(func, "field")?;
321                Ok(WindowFunctionSpec::FirstValue { field })
322            },
323            "last_value" => {
324                let field = Self::extract_string_field(func, "field")?;
325                Ok(WindowFunctionSpec::LastValue { field })
326            },
327            "nth_value" => {
328                let field = Self::extract_string_field(func, "field")?;
329                let n = u32::try_from(func.get("n").and_then(|v| v.as_u64()).ok_or_else(|| {
330                    FraiseQLError::Validation {
331                        message: "Missing 'n' in NTH_VALUE function".to_string(),
332                        path:    None,
333                    }
334                })?)
335                .unwrap_or(u32::MAX);
336                Ok(WindowFunctionSpec::NthValue { field, n })
337            },
338
339            // Aggregate as window functions
340            "running_sum" => {
341                let measure = Self::extract_string_field(func, "measure")?;
342                Ok(WindowFunctionSpec::RunningSum { measure })
343            },
344            "running_avg" => {
345                let measure = Self::extract_string_field(func, "measure")?;
346                Ok(WindowFunctionSpec::RunningAvg { measure })
347            },
348            "running_count" => {
349                if let Some(field) = func.get("field").and_then(|v| v.as_str()) {
350                    Ok(WindowFunctionSpec::RunningCountField {
351                        field: field.to_string(),
352                    })
353                } else {
354                    Ok(WindowFunctionSpec::RunningCount)
355                }
356            },
357            "running_min" => {
358                let measure = Self::extract_string_field(func, "measure")?;
359                Ok(WindowFunctionSpec::RunningMin { measure })
360            },
361            "running_max" => {
362                let measure = Self::extract_string_field(func, "measure")?;
363                Ok(WindowFunctionSpec::RunningMax { measure })
364            },
365            "running_stddev" => {
366                let measure = Self::extract_string_field(func, "measure")?;
367                Ok(WindowFunctionSpec::RunningStddev { measure })
368            },
369            "running_variance" => {
370                let measure = Self::extract_string_field(func, "measure")?;
371                Ok(WindowFunctionSpec::RunningVariance { measure })
372            },
373
374            _ => Err(FraiseQLError::Validation {
375                message: format!("Unknown window function type: {func_type}"),
376                path:    None,
377            }),
378        }
379    }
380
381    /// Extract a required string field from JSON object.
382    fn extract_string_field(obj: &Value, field_name: &str) -> Result<String> {
383        obj.get(field_name).and_then(|v| v.as_str()).map(String::from).ok_or_else(|| {
384            FraiseQLError::Validation {
385                message: format!("Missing '{field_name}' in function spec"),
386                path:    None,
387            }
388        })
389    }
390
391    /// Parse PARTITION BY from JSON array.
392    fn parse_partition_by(partition_array: &Value) -> Result<Vec<PartitionByColumn>> {
393        let Some(arr) = partition_array.as_array() else {
394            return Ok(vec![]);
395        };
396
397        arr.iter()
398            .map(|item| {
399                let col_type = item.get("type").and_then(|v| v.as_str()).ok_or_else(|| {
400                    FraiseQLError::Validation {
401                        message: "Missing 'type' in partitionBy column".to_string(),
402                        path:    None,
403                    }
404                })?;
405
406                match col_type {
407                    "dimension" => {
408                        let path = item
409                            .get("path")
410                            .and_then(|v| v.as_str())
411                            .ok_or_else(|| FraiseQLError::Validation {
412                                message: "Missing 'path' in dimension partition column".to_string(),
413                                path:    None,
414                            })?
415                            .to_string();
416                        Ok(PartitionByColumn::Dimension { path })
417                    },
418                    "filter" => {
419                        let name = item
420                            .get("name")
421                            .and_then(|v| v.as_str())
422                            .ok_or_else(|| FraiseQLError::Validation {
423                                message: "Missing 'name' in filter partition column".to_string(),
424                                path:    None,
425                            })?
426                            .to_string();
427                        Ok(PartitionByColumn::Filter { name })
428                    },
429                    "measure" => {
430                        let name = item
431                            .get("name")
432                            .and_then(|v| v.as_str())
433                            .ok_or_else(|| FraiseQLError::Validation {
434                                message: "Missing 'name' in measure partition column".to_string(),
435                                path:    None,
436                            })?
437                            .to_string();
438                        Ok(PartitionByColumn::Measure { name })
439                    },
440                    _ => Err(FraiseQLError::Validation {
441                        message: format!("Unknown partition column type: {col_type}"),
442                        path:    None,
443                    }),
444                }
445            })
446            .collect()
447    }
448
449    /// Parse ORDER BY from JSON array.
450    fn parse_order_by(order_array: &Value) -> Result<Vec<WindowOrderBy>> {
451        let Some(arr) = order_array.as_array() else {
452            return Ok(vec![]);
453        };
454
455        arr.iter()
456            .map(|item| {
457                let field = item
458                    .get("field")
459                    .and_then(|v| v.as_str())
460                    .ok_or_else(|| FraiseQLError::Validation {
461                        message: "Missing 'field' in orderBy".to_string(),
462                        path:    None,
463                    })?
464                    .to_string();
465
466                let direction = match item.get("direction").and_then(|v| v.as_str()) {
467                    Some("DESC" | "desc") => OrderDirection::Desc,
468                    _ => OrderDirection::Asc,
469                };
470
471                Ok(WindowOrderBy { field, direction })
472            })
473            .collect()
474    }
475
476    /// Parse WHERE clause from JSON: `{ "<field>_<operator>": value, … }`.
477    fn parse_where_clause(where_obj: &Value, metadata: &FactTableMetadata) -> Result<WhereClause> {
478        let Some(obj) = where_obj.as_object() else {
479            return Ok(WhereClause::And(vec![]));
480        };
481
482        let no_native_columns = std::collections::HashMap::new();
483        let mut conditions = Vec::new();
484        for (key, value) in obj {
485            let (field, operator) = split_where_key(key, metadata, &no_native_columns)?;
486            conditions.push(WhereClause::Field {
487                // Recase the JSONB key so a camelCase window filter
488                // (`organizationId_eq`) builds `data->>'organization_id'`
489                // rather than a never-matching `organizationId` key (#486).
490                path: vec![crate::utils::to_snake_case(field)],
491                operator,
492                value: value.clone(),
493            });
494        }
495
496        Ok(WhereClause::And(conditions))
497    }
498
499    /// Parse window frame from JSON.
500    fn parse_frame(frame: &Value) -> Result<WindowFrame> {
501        let frame_type = match frame.get("frame_type").and_then(|v| v.as_str()) {
502            Some("ROWS") => FrameType::Rows,
503            Some("RANGE") => FrameType::Range,
504            Some("GROUPS") => FrameType::Groups,
505            _ => {
506                return Err(FraiseQLError::Validation {
507                    message: "Invalid or missing 'frame_type' in frame".to_string(),
508                    path:    None,
509                });
510            },
511        };
512
513        let start = frame
514            .get("start")
515            .ok_or_else(|| FraiseQLError::Validation {
516                message: "Missing 'start' in frame".to_string(),
517                path:    None,
518            })
519            .and_then(Self::parse_frame_boundary)?;
520
521        let end = frame
522            .get("end")
523            .ok_or_else(|| FraiseQLError::Validation {
524                message: "Missing 'end' in frame".to_string(),
525                path:    None,
526            })
527            .and_then(Self::parse_frame_boundary)?;
528
529        let exclusion = frame.get("exclusion").and_then(|v| v.as_str()).map(|s| match s {
530            "current_row" => FrameExclusion::CurrentRow,
531            "group" => FrameExclusion::Group,
532            "ties" => FrameExclusion::Ties,
533            _ => FrameExclusion::NoOthers,
534        });
535
536        Ok(WindowFrame {
537            frame_type,
538            start,
539            end,
540            exclusion,
541        })
542    }
543
544    /// Parse frame boundary from JSON.
545    fn parse_frame_boundary(boundary: &Value) -> Result<FrameBoundary> {
546        let boundary_type = boundary.get("type").and_then(|v| v.as_str()).ok_or_else(|| {
547            FraiseQLError::Validation {
548                message: "Missing 'type' in frame boundary".to_string(),
549                path:    None,
550            }
551        })?;
552
553        match boundary_type {
554            "unbounded_preceding" => Ok(FrameBoundary::UnboundedPreceding),
555            "n_preceding" => {
556                let n =
557                    u32::try_from(boundary.get("n").and_then(|v| v.as_u64()).ok_or_else(|| {
558                        FraiseQLError::Validation {
559                            message: "Missing 'n' in N PRECEDING boundary".to_string(),
560                            path:    None,
561                        }
562                    })?)
563                    .unwrap_or(u32::MAX);
564                Ok(FrameBoundary::NPreceding { n })
565            },
566            "current_row" => Ok(FrameBoundary::CurrentRow),
567            "n_following" => {
568                let n =
569                    u32::try_from(boundary.get("n").and_then(|v| v.as_u64()).ok_or_else(|| {
570                        FraiseQLError::Validation {
571                            message: "Missing 'n' in N FOLLOWING boundary".to_string(),
572                            path:    None,
573                        }
574                    })?)
575                    .unwrap_or(u32::MAX);
576                Ok(FrameBoundary::NFollowing { n })
577            },
578            "unbounded_following" => Ok(FrameBoundary::UnboundedFollowing),
579            _ => Err(FraiseQLError::Validation {
580                message: format!("Unknown frame boundary type: {boundary_type}"),
581                path:    None,
582            }),
583        }
584    }
585}