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.get("limit").and_then(|v| v.as_u64()).map(|n| n as u32);
122
123        let offset = query_json.get("offset").and_then(|v| v.as_u64()).map(|n| n as u32);
124
125        Ok(WindowRequest {
126            table_name,
127            select,
128            windows,
129            where_clause,
130            order_by,
131            limit,
132            offset,
133        })
134    }
135
136    /// Parse SELECT columns from JSON array.
137    fn parse_select_columns(select_array: &Value) -> Result<Vec<WindowSelectColumn>> {
138        let Some(arr) = select_array.as_array() else {
139            return Ok(vec![]);
140        };
141
142        arr.iter().map(Self::parse_single_select_column).collect()
143    }
144
145    fn parse_single_select_column(col: &Value) -> Result<WindowSelectColumn> {
146        let col_type =
147            col.get("type")
148                .and_then(|v| v.as_str())
149                .ok_or_else(|| FraiseQLError::Validation {
150                    message: "Missing 'type' in select column".to_string(),
151                    path:    None,
152                })?;
153
154        let alias = col
155            .get("alias")
156            .and_then(|v| v.as_str())
157            .ok_or_else(|| FraiseQLError::Validation {
158                message: "Missing 'alias' in select column".to_string(),
159                path:    None,
160            })?
161            .to_string();
162
163        match col_type {
164            "measure" => {
165                let name = col
166                    .get("name")
167                    .and_then(|v| v.as_str())
168                    .ok_or_else(|| FraiseQLError::Validation {
169                        message: "Missing 'name' in measure select column".to_string(),
170                        path:    None,
171                    })?
172                    .to_string();
173                Ok(WindowSelectColumn::Measure { name, alias })
174            },
175            "dimension" => {
176                let path = col
177                    .get("path")
178                    .and_then(|v| v.as_str())
179                    .ok_or_else(|| FraiseQLError::Validation {
180                        message: "Missing 'path' in dimension select column".to_string(),
181                        path:    None,
182                    })?
183                    .to_string();
184                Ok(WindowSelectColumn::Dimension { path, alias })
185            },
186            "filter" => {
187                let name = col
188                    .get("name")
189                    .and_then(|v| v.as_str())
190                    .ok_or_else(|| FraiseQLError::Validation {
191                        message: "Missing 'name' in filter select column".to_string(),
192                        path:    None,
193                    })?
194                    .to_string();
195                Ok(WindowSelectColumn::Filter { name, alias })
196            },
197            _ => Err(FraiseQLError::Validation {
198                message: format!("Unknown select column type: {col_type}"),
199                path:    None,
200            }),
201        }
202    }
203
204    /// Parse window functions from JSON array.
205    fn parse_window_functions(windows_array: &Value) -> Result<Vec<WindowFunctionRequest>> {
206        let Some(arr) = windows_array.as_array() else {
207            return Ok(vec![]);
208        };
209
210        arr.iter().map(Self::parse_single_window_function).collect()
211    }
212
213    fn parse_single_window_function(window: &Value) -> Result<WindowFunctionRequest> {
214        // Parse function spec
215        let function = window
216            .get("function")
217            .ok_or_else(|| FraiseQLError::Validation {
218                message: "Missing 'function' in window definition".to_string(),
219                path:    None,
220            })
221            .and_then(Self::parse_function_spec)?;
222
223        // Parse alias
224        let alias = window
225            .get("alias")
226            .and_then(|v| v.as_str())
227            .ok_or_else(|| FraiseQLError::Validation {
228                message: "Missing 'alias' in window definition".to_string(),
229                path:    None,
230            })?
231            .to_string();
232
233        // Parse PARTITION BY
234        let partition_by = if let Some(partition_array) = window.get("partitionBy") {
235            Self::parse_partition_by(partition_array)?
236        } else {
237            vec![]
238        };
239
240        // Parse ORDER BY within window
241        let order_by = if let Some(order_array) = window.get("orderBy") {
242            Self::parse_order_by(order_array)?
243        } else {
244            vec![]
245        };
246
247        // Parse frame
248        let frame = window.get("frame").map(Self::parse_frame).transpose()?;
249
250        Ok(WindowFunctionRequest {
251            function,
252            alias,
253            partition_by,
254            order_by,
255            frame,
256        })
257    }
258
259    /// Parse window function specification.
260    fn parse_function_spec(func: &Value) -> Result<WindowFunctionSpec> {
261        let func_type =
262            func.get("type")
263                .and_then(|v| v.as_str())
264                .ok_or_else(|| FraiseQLError::Validation {
265                    message: "Missing 'type' in function spec".to_string(),
266                    path:    None,
267                })?;
268
269        match func_type {
270            // Ranking functions
271            "row_number" => Ok(WindowFunctionSpec::RowNumber),
272            "rank" => Ok(WindowFunctionSpec::Rank),
273            "dense_rank" => Ok(WindowFunctionSpec::DenseRank),
274            "ntile" => {
275                let n = func.get("n").and_then(|v| v.as_u64()).ok_or_else(|| {
276                    FraiseQLError::Validation {
277                        message: "Missing 'n' in NTILE function".to_string(),
278                        path:    None,
279                    }
280                })? as u32;
281                Ok(WindowFunctionSpec::Ntile { n })
282            },
283            "percent_rank" => Ok(WindowFunctionSpec::PercentRank),
284            "cume_dist" => Ok(WindowFunctionSpec::CumeDist),
285
286            // Value functions
287            "lag" => {
288                let field = Self::extract_string_field(func, "field")?;
289                let offset = func.get("offset").and_then(|v| v.as_i64()).unwrap_or(1) as i32;
290                let default = func.get("default").cloned();
291                Ok(WindowFunctionSpec::Lag {
292                    field,
293                    offset,
294                    default,
295                })
296            },
297            "lead" => {
298                let field = Self::extract_string_field(func, "field")?;
299                let offset = func.get("offset").and_then(|v| v.as_i64()).unwrap_or(1) as i32;
300                let default = func.get("default").cloned();
301                Ok(WindowFunctionSpec::Lead {
302                    field,
303                    offset,
304                    default,
305                })
306            },
307            "first_value" => {
308                let field = Self::extract_string_field(func, "field")?;
309                Ok(WindowFunctionSpec::FirstValue { field })
310            },
311            "last_value" => {
312                let field = Self::extract_string_field(func, "field")?;
313                Ok(WindowFunctionSpec::LastValue { field })
314            },
315            "nth_value" => {
316                let field = Self::extract_string_field(func, "field")?;
317                let n = func.get("n").and_then(|v| v.as_u64()).ok_or_else(|| {
318                    FraiseQLError::Validation {
319                        message: "Missing 'n' in NTH_VALUE function".to_string(),
320                        path:    None,
321                    }
322                })? as u32;
323                Ok(WindowFunctionSpec::NthValue { field, n })
324            },
325
326            // Aggregate as window functions
327            "running_sum" => {
328                let measure = Self::extract_string_field(func, "measure")?;
329                Ok(WindowFunctionSpec::RunningSum { measure })
330            },
331            "running_avg" => {
332                let measure = Self::extract_string_field(func, "measure")?;
333                Ok(WindowFunctionSpec::RunningAvg { measure })
334            },
335            "running_count" => {
336                if let Some(field) = func.get("field").and_then(|v| v.as_str()) {
337                    Ok(WindowFunctionSpec::RunningCountField {
338                        field: field.to_string(),
339                    })
340                } else {
341                    Ok(WindowFunctionSpec::RunningCount)
342                }
343            },
344            "running_min" => {
345                let measure = Self::extract_string_field(func, "measure")?;
346                Ok(WindowFunctionSpec::RunningMin { measure })
347            },
348            "running_max" => {
349                let measure = Self::extract_string_field(func, "measure")?;
350                Ok(WindowFunctionSpec::RunningMax { measure })
351            },
352            "running_stddev" => {
353                let measure = Self::extract_string_field(func, "measure")?;
354                Ok(WindowFunctionSpec::RunningStddev { measure })
355            },
356            "running_variance" => {
357                let measure = Self::extract_string_field(func, "measure")?;
358                Ok(WindowFunctionSpec::RunningVariance { measure })
359            },
360
361            _ => Err(FraiseQLError::Validation {
362                message: format!("Unknown window function type: {func_type}"),
363                path:    None,
364            }),
365        }
366    }
367
368    /// Extract a required string field from JSON object.
369    fn extract_string_field(obj: &Value, field_name: &str) -> Result<String> {
370        obj.get(field_name).and_then(|v| v.as_str()).map(String::from).ok_or_else(|| {
371            FraiseQLError::Validation {
372                message: format!("Missing '{field_name}' in function spec"),
373                path:    None,
374            }
375        })
376    }
377
378    /// Parse PARTITION BY from JSON array.
379    fn parse_partition_by(partition_array: &Value) -> Result<Vec<PartitionByColumn>> {
380        let Some(arr) = partition_array.as_array() else {
381            return Ok(vec![]);
382        };
383
384        arr.iter()
385            .map(|item| {
386                let col_type = item.get("type").and_then(|v| v.as_str()).ok_or_else(|| {
387                    FraiseQLError::Validation {
388                        message: "Missing 'type' in partitionBy column".to_string(),
389                        path:    None,
390                    }
391                })?;
392
393                match col_type {
394                    "dimension" => {
395                        let path = item
396                            .get("path")
397                            .and_then(|v| v.as_str())
398                            .ok_or_else(|| FraiseQLError::Validation {
399                                message: "Missing 'path' in dimension partition column".to_string(),
400                                path:    None,
401                            })?
402                            .to_string();
403                        Ok(PartitionByColumn::Dimension { path })
404                    },
405                    "filter" => {
406                        let name = item
407                            .get("name")
408                            .and_then(|v| v.as_str())
409                            .ok_or_else(|| FraiseQLError::Validation {
410                                message: "Missing 'name' in filter partition column".to_string(),
411                                path:    None,
412                            })?
413                            .to_string();
414                        Ok(PartitionByColumn::Filter { name })
415                    },
416                    "measure" => {
417                        let name = item
418                            .get("name")
419                            .and_then(|v| v.as_str())
420                            .ok_or_else(|| FraiseQLError::Validation {
421                                message: "Missing 'name' in measure partition column".to_string(),
422                                path:    None,
423                            })?
424                            .to_string();
425                        Ok(PartitionByColumn::Measure { name })
426                    },
427                    _ => Err(FraiseQLError::Validation {
428                        message: format!("Unknown partition column type: {col_type}"),
429                        path:    None,
430                    }),
431                }
432            })
433            .collect()
434    }
435
436    /// Parse ORDER BY from JSON array.
437    fn parse_order_by(order_array: &Value) -> Result<Vec<WindowOrderBy>> {
438        let Some(arr) = order_array.as_array() else {
439            return Ok(vec![]);
440        };
441
442        arr.iter()
443            .map(|item| {
444                let field = item
445                    .get("field")
446                    .and_then(|v| v.as_str())
447                    .ok_or_else(|| FraiseQLError::Validation {
448                        message: "Missing 'field' in orderBy".to_string(),
449                        path:    None,
450                    })?
451                    .to_string();
452
453                let direction = match item.get("direction").and_then(|v| v.as_str()) {
454                    Some("DESC" | "desc") => OrderDirection::Desc,
455                    _ => OrderDirection::Asc,
456                };
457
458                Ok(WindowOrderBy { field, direction })
459            })
460            .collect()
461    }
462
463    /// Parse WHERE clause from JSON.
464    fn parse_where_clause(where_obj: &Value) -> Result<WhereClause> {
465        let Some(obj) = where_obj.as_object() else {
466            return Ok(WhereClause::And(vec![]));
467        };
468
469        let mut conditions = Vec::new();
470
471        for (key, value) in obj {
472            // Parse field_operator format (e.g., "customer_id_eq" -> field="customer_id",
473            // operator="eq")
474            if let Some((field, operator_str)) = Self::parse_where_field_and_operator(key)? {
475                let operator = WhereOperator::from_str(operator_str)?;
476
477                conditions.push(WhereClause::Field {
478                    path: vec![field.to_string()],
479                    operator,
480                    value: value.clone(),
481                });
482            }
483        }
484
485        Ok(WhereClause::And(conditions))
486    }
487
488    /// Parse WHERE field and operator from key.
489    fn parse_where_field_and_operator(key: &str) -> Result<Option<(&str, &str)>> {
490        if let Some(last_underscore) = key.rfind('_') {
491            let field = &key[..last_underscore];
492            let operator = &key[last_underscore + 1..];
493
494            match WhereOperator::from_str(operator) {
495                Ok(_) => Ok(Some((field, operator))),
496                Err(_) => Ok(None),
497            }
498        } else {
499            Ok(None)
500        }
501    }
502
503    /// Parse window frame from JSON.
504    fn parse_frame(frame: &Value) -> Result<WindowFrame> {
505        let frame_type = match frame.get("frame_type").and_then(|v| v.as_str()) {
506            Some("ROWS") => FrameType::Rows,
507            Some("RANGE") => FrameType::Range,
508            Some("GROUPS") => FrameType::Groups,
509            _ => {
510                return Err(FraiseQLError::Validation {
511                    message: "Invalid or missing 'frame_type' in frame".to_string(),
512                    path:    None,
513                });
514            },
515        };
516
517        let start = frame
518            .get("start")
519            .ok_or_else(|| FraiseQLError::Validation {
520                message: "Missing 'start' in frame".to_string(),
521                path:    None,
522            })
523            .and_then(Self::parse_frame_boundary)?;
524
525        let end = frame
526            .get("end")
527            .ok_or_else(|| FraiseQLError::Validation {
528                message: "Missing 'end' in frame".to_string(),
529                path:    None,
530            })
531            .and_then(Self::parse_frame_boundary)?;
532
533        let exclusion = frame.get("exclusion").and_then(|v| v.as_str()).map(|s| match s {
534            "current_row" => FrameExclusion::CurrentRow,
535            "group" => FrameExclusion::Group,
536            "ties" => FrameExclusion::Ties,
537            _ => FrameExclusion::NoOthers,
538        });
539
540        Ok(WindowFrame {
541            frame_type,
542            start,
543            end,
544            exclusion,
545        })
546    }
547
548    /// Parse frame boundary from JSON.
549    fn parse_frame_boundary(boundary: &Value) -> Result<FrameBoundary> {
550        let boundary_type = boundary.get("type").and_then(|v| v.as_str()).ok_or_else(|| {
551            FraiseQLError::Validation {
552                message: "Missing 'type' in frame boundary".to_string(),
553                path:    None,
554            }
555        })?;
556
557        match boundary_type {
558            "unbounded_preceding" => Ok(FrameBoundary::UnboundedPreceding),
559            "n_preceding" => {
560                let n = boundary.get("n").and_then(|v| v.as_u64()).ok_or_else(|| {
561                    FraiseQLError::Validation {
562                        message: "Missing 'n' in N PRECEDING boundary".to_string(),
563                        path:    None,
564                    }
565                })? as u32;
566                Ok(FrameBoundary::NPreceding { n })
567            },
568            "current_row" => Ok(FrameBoundary::CurrentRow),
569            "n_following" => {
570                let n = boundary.get("n").and_then(|v| v.as_u64()).ok_or_else(|| {
571                    FraiseQLError::Validation {
572                        message: "Missing 'n' in N FOLLOWING boundary".to_string(),
573                        path:    None,
574                    }
575                })? as u32;
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}
586
587#[cfg(test)]
588mod tests {
589    use serde_json::json;
590
591    use super::*;
592    use crate::compiler::fact_table::{DimensionColumn, FilterColumn, MeasureColumn, SqlType};
593
594    fn create_test_metadata() -> FactTableMetadata {
595        FactTableMetadata {
596            table_name:           "tf_sales".to_string(),
597            measures:             vec![
598                MeasureColumn {
599                    name:     "revenue".to_string(),
600                    sql_type: SqlType::Decimal,
601                    nullable: false,
602                },
603                MeasureColumn {
604                    name:     "quantity".to_string(),
605                    sql_type: SqlType::Int,
606                    nullable: false,
607                },
608            ],
609            dimensions:           DimensionColumn {
610                name:  "dimensions".to_string(),
611                paths: vec![],
612            },
613            denormalized_filters: vec![
614                FilterColumn {
615                    name:     "customer_id".to_string(),
616                    sql_type: SqlType::Uuid,
617                    indexed:  true,
618                },
619                FilterColumn {
620                    name:     "occurred_at".to_string(),
621                    sql_type: SqlType::Timestamp,
622                    indexed:  true,
623                },
624            ],
625            calendar_dimensions:  vec![],
626        }
627    }
628
629    #[test]
630    fn test_parse_simple_window_query() {
631        let metadata = create_test_metadata();
632        let query = json!({
633            "table": "tf_sales",
634            "select": [
635                {"type": "measure", "name": "revenue", "alias": "revenue"}
636            ],
637            "windows": [
638                {
639                    "function": {"type": "row_number"},
640                    "alias": "rank",
641                    "partitionBy": [{"type": "dimension", "path": "category"}],
642                    "orderBy": [{"field": "revenue", "direction": "DESC"}]
643                }
644            ]
645        });
646
647        let request = WindowQueryParser::parse(&query, &metadata).unwrap();
648
649        assert_eq!(request.table_name, "tf_sales");
650        assert_eq!(request.select.len(), 1);
651        assert_eq!(request.windows.len(), 1);
652        assert_eq!(request.windows[0].alias, "rank");
653        assert!(matches!(request.windows[0].function, WindowFunctionSpec::RowNumber));
654    }
655
656    #[test]
657    fn test_parse_running_sum() {
658        let metadata = create_test_metadata();
659        let query = json!({
660            "table": "tf_sales",
661            "select": [],
662            "windows": [
663                {
664                    "function": {"type": "running_sum", "measure": "revenue"},
665                    "alias": "running_total",
666                    "orderBy": [{"field": "occurred_at", "direction": "ASC"}],
667                    "frame": {
668                        "frame_type": "ROWS",
669                        "start": {"type": "unbounded_preceding"},
670                        "end": {"type": "current_row"}
671                    }
672                }
673            ]
674        });
675
676        let request = WindowQueryParser::parse(&query, &metadata).unwrap();
677
678        assert_eq!(request.windows.len(), 1);
679        match &request.windows[0].function {
680            WindowFunctionSpec::RunningSum { measure } => {
681                assert_eq!(measure, "revenue");
682            },
683            _ => panic!("Expected RunningSum function"),
684        }
685        assert!(request.windows[0].frame.is_some());
686    }
687
688    #[test]
689    fn test_parse_lag_function() {
690        let metadata = create_test_metadata();
691        let query = json!({
692            "table": "tf_sales",
693            "windows": [
694                {
695                    "function": {"type": "lag", "field": "revenue", "offset": 1, "default": 0},
696                    "alias": "prev_revenue",
697                    "orderBy": [{"field": "occurred_at"}]
698                }
699            ]
700        });
701
702        let request = WindowQueryParser::parse(&query, &metadata).unwrap();
703
704        match &request.windows[0].function {
705            WindowFunctionSpec::Lag {
706                field,
707                offset,
708                default,
709            } => {
710                assert_eq!(field, "revenue");
711                assert_eq!(*offset, 1);
712                assert!(default.is_some());
713            },
714            _ => panic!("Expected Lag function"),
715        }
716    }
717
718    #[test]
719    fn test_parse_ntile_function() {
720        let metadata = create_test_metadata();
721        let query = json!({
722            "table": "tf_sales",
723            "windows": [
724                {
725                    "function": {"type": "ntile", "n": 4},
726                    "alias": "quartile",
727                    "orderBy": [{"field": "revenue", "direction": "DESC"}]
728                }
729            ]
730        });
731
732        let request = WindowQueryParser::parse(&query, &metadata).unwrap();
733
734        match &request.windows[0].function {
735            WindowFunctionSpec::Ntile { n } => {
736                assert_eq!(*n, 4);
737            },
738            _ => panic!("Expected Ntile function"),
739        }
740    }
741
742    #[test]
743    fn test_parse_select_columns() {
744        let metadata = create_test_metadata();
745        let query = json!({
746            "table": "tf_sales",
747            "select": [
748                {"type": "measure", "name": "revenue", "alias": "rev"},
749                {"type": "dimension", "path": "category", "alias": "cat"},
750                {"type": "filter", "name": "occurred_at", "alias": "date"}
751            ]
752        });
753
754        let request = WindowQueryParser::parse(&query, &metadata).unwrap();
755
756        assert_eq!(request.select.len(), 3);
757        assert!(matches!(
758            &request.select[0],
759            WindowSelectColumn::Measure { name, alias } if name == "revenue" && alias == "rev"
760        ));
761        assert!(matches!(
762            &request.select[1],
763            WindowSelectColumn::Dimension { path, alias } if path == "category" && alias == "cat"
764        ));
765        assert!(matches!(
766            &request.select[2],
767            WindowSelectColumn::Filter { name, alias } if name == "occurred_at" && alias == "date"
768        ));
769    }
770
771    #[test]
772    fn test_parse_partition_by() {
773        let metadata = create_test_metadata();
774        let query = json!({
775            "table": "tf_sales",
776            "windows": [
777                {
778                    "function": {"type": "row_number"},
779                    "alias": "rank",
780                    "partitionBy": [
781                        {"type": "dimension", "path": "category"},
782                        {"type": "filter", "name": "customer_id"}
783                    ],
784                    "orderBy": []
785                }
786            ]
787        });
788
789        let request = WindowQueryParser::parse(&query, &metadata).unwrap();
790
791        assert_eq!(request.windows[0].partition_by.len(), 2);
792        assert!(matches!(
793            &request.windows[0].partition_by[0],
794            PartitionByColumn::Dimension { path } if path == "category"
795        ));
796        assert!(matches!(
797            &request.windows[0].partition_by[1],
798            PartitionByColumn::Filter { name } if name == "customer_id"
799        ));
800    }
801
802    #[test]
803    fn test_parse_limit_offset() {
804        let metadata = create_test_metadata();
805        let query = json!({
806            "table": "tf_sales",
807            "limit": 100,
808            "offset": 50
809        });
810
811        let request = WindowQueryParser::parse(&query, &metadata).unwrap();
812
813        assert_eq!(request.limit, Some(100));
814        assert_eq!(request.offset, Some(50));
815    }
816
817    #[test]
818    fn test_parse_final_order_by() {
819        let metadata = create_test_metadata();
820        let query = json!({
821            "table": "tf_sales",
822            "orderBy": [
823                {"field": "revenue", "direction": "DESC"},
824                {"field": "occurred_at", "direction": "ASC"}
825            ]
826        });
827
828        let request = WindowQueryParser::parse(&query, &metadata).unwrap();
829
830        assert_eq!(request.order_by.len(), 2);
831        assert_eq!(request.order_by[0].field, "revenue");
832        assert_eq!(request.order_by[0].direction, OrderDirection::Desc);
833        assert_eq!(request.order_by[1].field, "occurred_at");
834        assert_eq!(request.order_by[1].direction, OrderDirection::Asc);
835    }
836
837    #[test]
838    fn test_parse_complex_window_query() {
839        let metadata = create_test_metadata();
840        let query = json!({
841            "table": "tf_sales",
842            "select": [
843                {"type": "measure", "name": "revenue", "alias": "revenue"},
844                {"type": "dimension", "path": "category", "alias": "category"}
845            ],
846            "windows": [
847                {
848                    "function": {"type": "row_number"},
849                    "alias": "rank",
850                    "partitionBy": [{"type": "dimension", "path": "category"}],
851                    "orderBy": [{"field": "revenue", "direction": "DESC"}]
852                },
853                {
854                    "function": {"type": "running_sum", "measure": "revenue"},
855                    "alias": "running_total",
856                    "partitionBy": [{"type": "dimension", "path": "category"}],
857                    "orderBy": [{"field": "occurred_at", "direction": "ASC"}],
858                    "frame": {
859                        "frame_type": "ROWS",
860                        "start": {"type": "unbounded_preceding"},
861                        "end": {"type": "current_row"}
862                    }
863                },
864                {
865                    "function": {"type": "lag", "field": "revenue", "offset": 1},
866                    "alias": "prev_revenue",
867                    "partitionBy": [{"type": "dimension", "path": "category"}],
868                    "orderBy": [{"field": "occurred_at", "direction": "ASC"}]
869                }
870            ],
871            "orderBy": [
872                {"field": "category", "direction": "ASC"},
873                {"field": "revenue", "direction": "DESC"}
874            ],
875            "limit": 100
876        });
877
878        let request = WindowQueryParser::parse(&query, &metadata).unwrap();
879
880        assert_eq!(request.table_name, "tf_sales");
881        assert_eq!(request.select.len(), 2);
882        assert_eq!(request.windows.len(), 3);
883        assert_eq!(request.order_by.len(), 2);
884        assert_eq!(request.limit, Some(100));
885    }
886
887    #[test]
888    fn test_parse_error_missing_table() {
889        let metadata = create_test_metadata();
890        let query = json!({
891            "select": [],
892            "windows": []
893        });
894
895        let result = WindowQueryParser::parse(&query, &metadata);
896        assert!(result.is_err());
897        assert!(result.unwrap_err().to_string().contains("table"));
898    }
899
900    #[test]
901    fn test_parse_error_invalid_function_type() {
902        let metadata = create_test_metadata();
903        let query = json!({
904            "table": "tf_sales",
905            "windows": [
906                {
907                    "function": {"type": "invalid_function"},
908                    "alias": "test"
909                }
910            ]
911        });
912
913        let result = WindowQueryParser::parse(&query, &metadata);
914        assert!(result.is_err());
915        assert!(result.unwrap_err().to_string().contains("Unknown"));
916    }
917}