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