1use 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
67pub struct WindowQueryParser;
69
70impl WindowQueryParser {
71 pub fn parse(query_json: &Value, _metadata: &FactTableMetadata) -> Result<WindowRequest> {
82 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 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 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 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 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 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 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 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 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 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 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 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 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 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 "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 "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 "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 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 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 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 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 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 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 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 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}