1use super::error::ParseError;
4use crate::ast::{
5 BinOp, CompareOp, Expr, FieldRef, Filter, OrderByClause, Projection, QueryExpr,
6 QueueSelectQuery, SelectItem, Span, TableQuery, UnaryOp,
7};
8use crate::lexer::Token;
9use crate::sql_lowering::{expr_to_projection, filter_to_expr, select_item_to_projection};
10use reddb_types::types::Value;
11
12fn is_scalar_function(name: &str) -> bool {
13 matches!(
14 name,
15 "GEO_DISTANCE"
16 | "GEO_DISTANCE_VINCENTY"
17 | "GEO_BEARING"
18 | "GEO_MIDPOINT"
19 | "HAVERSINE"
20 | "VINCENTY"
21 | "TIME_BUCKET"
22 | "UPPER"
23 | "LOWER"
24 | "LENGTH"
25 | "CHAR_LENGTH"
26 | "CHARACTER_LENGTH"
27 | "OCTET_LENGTH"
28 | "BIT_LENGTH"
29 | "SUBSTRING"
30 | "SUBSTR"
31 | "POSITION"
32 | "TRIM"
33 | "LTRIM"
34 | "RTRIM"
35 | "BTRIM"
36 | "CONCAT"
37 | "CONCAT_WS"
38 | "REVERSE"
39 | "LEFT"
40 | "RIGHT"
41 | "QUOTE_LITERAL"
42 | "ABS"
43 | "ROUND"
44 | "COALESCE"
45 | "STDDEV"
46 | "VARIANCE"
47 | "MEDIAN"
48 | "PERCENTILE"
49 | "GROUP_CONCAT"
50 | "STRING_AGG"
51 | "FIRST"
52 | "LAST"
53 | "ARRAY_AGG"
54 | "COUNT_DISTINCT"
55 | "MONEY"
56 | "MONEY_ASSET"
57 | "MONEY_MINOR"
58 | "MONEY_SCALE"
59 | "VERIFY_PASSWORD"
60 | "CAST"
61 | "CASE"
62 )
63}
64
65fn is_aggregate_function(name: &str) -> bool {
66 matches!(
67 name,
68 "COUNT"
69 | "AVG"
70 | "SUM"
71 | "MIN"
72 | "MAX"
73 | "STDDEV"
74 | "VARIANCE"
75 | "MEDIAN"
76 | "PERCENTILE"
77 | "GROUP_CONCAT"
78 | "STRING_AGG"
79 | "FIRST"
80 | "LAST"
81 | "ARRAY_AGG"
82 | "COUNT_DISTINCT"
83 )
84}
85
86fn aggregate_token_name(token: &Token) -> Option<&'static str> {
87 match token {
88 Token::Count => Some("COUNT"),
89 Token::Sum => Some("SUM"),
90 Token::Avg => Some("AVG"),
91 Token::Min => Some("MIN"),
92 Token::Max => Some("MAX"),
93 Token::First => Some("FIRST"),
94 Token::Last => Some("LAST"),
95 _ => None,
96 }
97}
98
99fn scalar_token_name(token: &Token) -> Option<&'static str> {
100 match token {
101 Token::Left => Some("LEFT"),
102 Token::Right => Some("RIGHT"),
103 _ => None,
104 }
105}
106use super::Parser;
107
108impl<'a> Parser<'a> {
109 pub fn parse_select_query(&mut self) -> Result<QueryExpr, ParseError> {
111 self.enter_depth()?;
116 let result = self.parse_select_query_inner();
117 self.exit_depth();
118 result
119 }
120
121 #[allow(clippy::type_complexity)]
137 fn parse_table_function_args(
138 &mut self,
139 name: &str,
140 ) -> Result<(Vec<String>, Vec<(String, f64)>, Vec<(String, QueryExpr)>), ParseError> {
141 if matches!(self.peek(), Token::RParen) {
143 return Err(ParseError::new(
144 format!("table function '{name}' requires at least one argument"),
145 self.position(),
146 ));
147 }
148
149 let mut args = Vec::new();
150 let mut named_args = Vec::new();
151 let mut subquery_args: Vec<(String, QueryExpr)> = Vec::new();
152 loop {
153 let ident = match self.advance()? {
159 Token::Ident(arg) | Token::String(arg) => arg,
160 Token::MaxIterations => "max_iterations".to_string(),
161 other => {
162 return Err(ParseError::expected(
163 vec!["table function argument identifier or string literal"],
164 &other,
165 self.position(),
166 ));
167 }
168 };
169
170 if matches!(self.peek(), Token::FatArrow) {
174 self.advance()?; if matches!(self.peek(), Token::LParen) {
176 self.advance()?; if !self.check(&Token::Select) {
178 let found = self.peek().clone();
179 return Err(ParseError::expected(
180 vec!["SELECT subquery"],
181 &found,
182 self.position(),
183 ));
184 }
185 let query = self.parse_select_query()?;
186 self.expect(Token::RParen)?;
187 subquery_args.push((ident, query));
188 } else {
189 let value = self.parse_float()?;
190 named_args.push((ident, value));
191 }
192 } else if named_args.is_empty() && subquery_args.is_empty() {
193 args.push(ident);
194 } else {
195 return Err(ParseError::new(
196 format!(
197 "table function '{name}' positional argument '{ident}' cannot follow a named argument"
198 ),
199 self.position(),
200 ));
201 }
202
203 match self.peek() {
206 Token::Comma => {
207 self.advance()?;
208 continue;
209 }
210 Token::RParen => break,
211 _ => {
212 let found = self.peek().clone();
213 return Err(ParseError::expected(
214 vec!["','", "')'"],
215 &found,
216 self.position(),
217 ));
218 }
219 }
220 }
221 Ok((args, named_args, subquery_args))
222 }
223
224 fn build_table_function_source(
229 &self,
230 name: String,
231 args: Vec<String>,
232 named_args: Vec<(String, f64)>,
233 subquery_args: Vec<(String, QueryExpr)>,
234 ) -> Result<crate::ast::TableSource, ParseError> {
235 use crate::ast::TableSource;
236
237 if subquery_args.is_empty() {
238 return Ok(TableSource::Function {
239 name,
240 args,
241 named_args,
242 });
243 }
244
245 if !args.is_empty() {
248 return Err(ParseError::new(
249 format!(
250 "table function '{name}' inline form takes no positional graph argument; pass `nodes => (…), edges => (…)`"
251 ),
252 self.position(),
253 ));
254 }
255
256 let mut nodes: Option<QueryExpr> = None;
257 let mut edges: Option<QueryExpr> = None;
258 for (key, query) in subquery_args {
259 if key.eq_ignore_ascii_case("nodes") {
260 if nodes.is_some() {
261 return Err(ParseError::new(
262 format!(
263 "table function '{name}' has a duplicate 'nodes' subquery argument"
264 ),
265 self.position(),
266 ));
267 }
268 nodes = Some(query);
269 } else if key.eq_ignore_ascii_case("edges") {
270 if edges.is_some() {
271 return Err(ParseError::new(
272 format!(
273 "table function '{name}' has a duplicate 'edges' subquery argument"
274 ),
275 self.position(),
276 ));
277 }
278 edges = Some(query);
279 } else {
280 return Err(ParseError::new(
281 format!(
282 "table function '{name}' has no subquery argument '{key}' (expected 'nodes' or 'edges')"
283 ),
284 self.position(),
285 ));
286 }
287 }
288
289 let (Some(nodes), Some(edges)) = (nodes, edges) else {
290 return Err(ParseError::new(
291 format!(
292 "table function '{name}' inline form requires both `nodes => (…)` and `edges => (…)` subqueries"
293 ),
294 self.position(),
295 ));
296 };
297
298 Ok(TableSource::InlineGraphFunction {
299 name,
300 nodes: Box::new(nodes),
301 edges: Box::new(edges),
302 named_args,
303 })
304 }
305
306 fn parse_table_name_segment(&mut self) -> Result<String, ParseError> {
312 match self.peek() {
313 Token::Components => {
314 self.advance()?;
315 Ok("components".to_string())
316 }
317 Token::Centrality => {
318 self.advance()?;
319 Ok("centrality".to_string())
320 }
321 _ => self.expect_ident(),
322 }
323 }
324
325 fn parse_select_query_inner(&mut self) -> Result<QueryExpr, ParseError> {
326 self.expect(Token::Select)?;
327
328 let distinct = self.consume(&Token::Distinct)?;
333
334 let (select_items, columns) = self.parse_select_items_and_projections()?;
336
337 let has_from = self.consume(&Token::From)?;
340 let mut table_source: Option<crate::ast::TableSource> = None;
344 let mut from_subquery: Option<QueryExpr> = None;
345 let table = if has_from {
346 if self.consume(&Token::Queue)? {
347 let queue = self.expect_ident()?;
348 let filter = if self.consume(&Token::Where)? {
349 Some(self.parse_filter()?)
350 } else {
351 None
352 };
353 let limit = if self.consume(&Token::Limit)? {
354 Some(self.parse_integer()? as u64)
355 } else {
356 None
357 };
358 return Ok(QueryExpr::QueueSelect(QueueSelectQuery {
359 queue,
360 columns: queue_projection_columns(&columns)?,
361 filter,
362 limit,
363 }));
364 } else if self.check(&Token::LParen) {
365 self.advance()?; if !self.check(&Token::Select) {
367 return Err(ParseError::new(
368 "subquery in FROM must start with SELECT".to_string(),
369 self.position(),
370 ));
371 }
372 from_subquery = Some(self.parse_select_query()?);
373 self.expect(Token::RParen)?;
374 "__subq_pending".to_string()
375 } else if self.consume(&Token::Star)? {
376 "*".to_string()
377 } else if self.consume(&Token::All)? {
378 "all".to_string()
379 } else if matches!(self.peek(), Token::Components) {
380 self.advance()?; let name = "components".to_string();
386 self.expect(Token::LParen)?;
387 let (args, named_args, subquery_args) = self.parse_table_function_args(&name)?;
388 self.expect(Token::RParen)?;
389 table_source = Some(self.build_table_function_source(
390 name.clone(),
391 args,
392 named_args,
393 subquery_args,
394 )?);
395 name
396 } else if matches!(self.peek(), Token::ShortestPath) {
397 self.advance()?; let name = "shortest_path".to_string();
406 self.expect(Token::LParen)?;
407 let (args, named_args, subquery_args) = self.parse_table_function_args(&name)?;
408 self.expect(Token::RParen)?;
409 table_source = Some(self.build_table_function_source(
410 name.clone(),
411 args,
412 named_args,
413 subquery_args,
414 )?);
415 name
416 } else {
417 let ident = self.expect_ident()?;
418 let mut name = ident;
419 while matches!(self.peek(), Token::Dot) {
420 self.advance()?; let segment = self.parse_table_name_segment()?;
422 name.push('.');
423 name.push_str(&segment);
424 }
425 if matches!(self.peek(), Token::LParen) {
427 self.advance()?; let (args, named_args, subquery_args) =
429 self.parse_table_function_args(&name)?;
430 self.expect(Token::RParen)?;
431 table_source = Some(self.build_table_function_source(
432 name.clone(),
433 args,
434 named_args,
435 subquery_args,
436 )?);
437 name
438 } else {
439 name
440 }
441 }
442 } else {
443 "any".to_string()
444 };
445
446 let alias =
450 if !has_from || (self.check(&Token::As) && matches!(self.peek_next()?, Token::Of)) {
451 None
452 } else if self.consume(&Token::As)?
453 || (self.check(&Token::Ident("".into())) && !self.is_clause_keyword())
454 {
455 Some(self.expect_ident()?)
456 } else {
457 None
458 };
459
460 let mut query = if let Some(inner) = from_subquery {
461 let mut query = TableQuery::from_subquery(inner, alias);
462 query.select_items = select_items;
463 query.columns = columns;
464 query.distinct = distinct;
465 query
466 } else {
467 TableQuery {
468 table,
469 source: table_source,
470 alias,
471 select_items,
472 columns,
473 where_expr: None,
474 filter: None,
475 group_by_exprs: Vec::new(),
476 group_by: Vec::new(),
477 having_expr: None,
478 having: None,
479 order_by: Vec::new(),
480 limit: None,
481 limit_param: None,
482 offset: None,
483 offset_param: None,
484 expand: None,
485 as_of: None,
486 sessionize: None,
487 distinct,
488 }
489 };
490
491 if self.is_join_keyword() {
492 let return_items = std::mem::take(&mut query.select_items);
493 let return_ = std::mem::take(&mut query.columns);
494 let mut expr = self.parse_join_query(QueryExpr::Table(query))?;
495 if let QueryExpr::Join(join) = &mut expr {
496 join.return_items = return_items;
497 join.return_ = return_;
498 }
499 return Ok(expr);
500 }
501
502 if self.consume(&Token::Sessionize)? {
513 query.sessionize = Some(self.parse_sessionize_clause()?);
514 }
515
516 self.parse_table_clauses(&mut query)?;
518
519 Ok(QueryExpr::Table(query))
520 }
521
522 fn parse_sessionize_clause(&mut self) -> Result<crate::ast::SessionizeClause, ParseError> {
523 use crate::ast::SessionizeClause;
524
525 let mut clause = SessionizeClause::default();
526
527 if self.consume(&Token::By)? {
528 clause.actor_col = Some(self.expect_ident()?);
529 }
530 if self.consume(&Token::Gap)? {
531 let value = self.parse_float()?;
532 let unit = self.parse_duration_unit()?;
533 clause.gap_ms = Some((value * unit) as u64);
534 }
535 if self.consume(&Token::Order)? {
540 self.expect(Token::By)?;
541 clause.order_col = Some(self.expect_ident()?);
542 }
543 Ok(clause)
544 }
545}
546
547impl<'a> Parser<'a> {
548 pub fn is_clause_keyword(&self) -> bool {
550 matches!(
551 self.peek(),
552 Token::Where
553 | Token::Order
554 | Token::Limit
555 | Token::Offset
556 | Token::Join
557 | Token::Inner
558 | Token::Left
559 | Token::Right
560 | Token::As
561 | Token::Sessionize
562 )
563 }
564
565 pub fn parse_projection_list(&mut self) -> Result<Vec<Projection>, ParseError> {
567 Ok(self.parse_select_items_and_projections()?.1)
568 }
569
570 pub(crate) fn parse_select_items_and_projections(
571 &mut self,
572 ) -> Result<(Vec<SelectItem>, Vec<Projection>), ParseError> {
573 if self.consume(&Token::Star)? {
575 return Ok((vec![SelectItem::Wildcard], Vec::new())); }
577
578 let mut select_items = Vec::new();
579 let mut projections = Vec::new();
580 loop {
581 let (item, proj) = self.parse_projection()?;
582 select_items.push(item);
583 projections.push(proj);
584
585 if !self.consume(&Token::Comma)? {
586 break;
587 }
588 }
589 Ok((select_items, projections))
590 }
591
592 fn parse_projection(&mut self) -> Result<(SelectItem, Projection), ParseError> {
594 let expr = self.parse_expr()?;
595 if contains_nested_aggregate(&expr) && !is_plain_aggregate_expr(&expr) {
596 return Err(ParseError::new(
597 "aggregate function is not valid inside another expression".to_string(),
598 self.position(),
599 ));
600 }
601 let alias = if self.consume(&Token::As)? {
602 Some(self.expect_column_ident()?)
603 } else {
604 None
605 };
606 let select_item = SelectItem::Expr {
607 expr: expr.clone(),
608 alias: alias.clone(),
609 };
610 let projection = select_item_to_projection(&select_item).ok_or_else(|| {
611 ParseError::new(
612 "projection cannot yet be lowered to legacy runtime representation".to_string(),
613 self.position(),
614 )
615 })?;
616 Ok((select_item, projection))
617 }
618}
619
620fn contains_nested_aggregate(expr: &Expr) -> bool {
621 match expr {
622 Expr::FunctionCall { name, args, .. } => {
623 is_aggregate_function(&name.to_uppercase())
624 || args.iter().any(contains_nested_aggregate)
625 }
626 Expr::WindowFunctionCall { args, window, .. } => {
633 args.iter().any(contains_nested_aggregate)
634 || window.partition_by.iter().any(contains_nested_aggregate)
635 || window
636 .order_by
637 .iter()
638 .any(|o| contains_nested_aggregate(&o.expr))
639 }
640 Expr::BinaryOp { lhs, rhs, .. } => {
641 contains_nested_aggregate(lhs) || contains_nested_aggregate(rhs)
642 }
643 Expr::UnaryOp { operand, .. } | Expr::IsNull { operand, .. } => {
644 contains_nested_aggregate(operand)
645 }
646 Expr::Cast { inner, .. } => contains_nested_aggregate(inner),
647 Expr::Case {
648 branches, else_, ..
649 } => {
650 branches.iter().any(|(cond, value)| {
651 contains_nested_aggregate(cond) || contains_nested_aggregate(value)
652 }) || else_.as_deref().is_some_and(contains_nested_aggregate)
653 }
654 Expr::InList { target, values, .. } => {
655 contains_nested_aggregate(target) || values.iter().any(contains_nested_aggregate)
656 }
657 Expr::Between {
658 target, low, high, ..
659 } => {
660 contains_nested_aggregate(target)
661 || contains_nested_aggregate(low)
662 || contains_nested_aggregate(high)
663 }
664 Expr::Literal { .. }
665 | Expr::Column { .. }
666 | Expr::Parameter { .. }
667 | Expr::Subquery { .. } => false,
668 }
669}
670
671fn is_plain_aggregate_expr(expr: &Expr) -> bool {
672 match expr {
673 Expr::FunctionCall { name, args, .. } if is_aggregate_function(&name.to_uppercase()) => {
674 !args.iter().any(contains_nested_aggregate)
675 }
676 _ => false,
677 }
678}
679
680fn attach_projection_alias(proj: Projection, alias: Option<String>) -> Projection {
681 let Some(alias) = alias else { return proj };
682 match proj {
683 Projection::Field(field, _) => Projection::Field(field, Some(alias)),
684 Projection::Expression(filter, _) => Projection::Expression(filter, Some(alias)),
685 Projection::Function(name, args) => {
686 if name.contains(':') {
687 Projection::Function(name, args)
688 } else {
689 Projection::Function(format!("{name}:{alias}"), args)
690 }
691 }
692 Projection::Column(column) => Projection::Alias(column, alias),
693 Projection::Window {
694 name, args, window, ..
695 } => Projection::Window {
696 name,
697 args,
698 window,
699 alias: Some(alias),
700 },
701 other => other,
702 }
703}
704
705fn queue_projection_columns(columns: &[Projection]) -> Result<Vec<String>, ParseError> {
706 let mut out = Vec::new();
707 for column in columns {
708 match column {
709 Projection::Column(name) => out.push(name.clone()),
710 Projection::Alias(name, _) => out.push(name.clone()),
711 Projection::Field(FieldRef::TableColumn { table, column }, _) if table.is_empty() => {
712 out.push(column.clone());
713 }
714 Projection::All => return Ok(Vec::new()),
715 other => {
716 return Err(ParseError::new(
717 format!(
718 "unsupported SELECT FROM QUEUE projection {other:?}; use `SELECT *` or bare column names, or use queue verbs (PUSH, POP, PEEK, LEN, ACK, NACK, …) for queue operations"
719 ),
720 crate::lexer::Position::default(),
721 ));
722 }
723 }
724 }
725 Ok(out)
726}
727
728impl<'a> Parser<'a> {
729 pub fn parse_table_clauses(&mut self, query: &mut TableQuery) -> Result<(), ParseError> {
731 if self.check(&Token::As) {
734 let next_is_of = matches!(self.peek_next()?, Token::Of);
735 if next_is_of {
736 self.expect(Token::As)?;
737 self.expect(Token::Of)?;
738 query.as_of = Some(self.parse_as_of_spec()?);
739 }
740 }
741
742 if self.consume(&Token::Where)? {
744 let filter = self.parse_filter()?;
745 query.where_expr = Some(filter_to_expr(&filter));
746 query.filter = Some(filter);
747 }
748
749 if self.consume(&Token::Group)? {
751 self.expect(Token::By)?;
752 let (group_by_exprs, group_by) = self.parse_group_by_items()?;
753 query.group_by_exprs = group_by_exprs;
754 query.group_by = group_by;
755 }
756
757 if !query.group_by_exprs.is_empty() && self.consume_ident_ci("HAVING")? {
759 let having = self.parse_filter()?;
760 query.having_expr = Some(filter_to_expr(&having));
761 query.having = Some(having);
762 }
763
764 if self.consume(&Token::Order)? {
766 self.expect(Token::By)?;
767 query.order_by = self.parse_order_by_list()?;
768 }
769
770 if self.consume(&Token::Limit)? {
772 if matches!(self.peek(), Token::Dollar | Token::Question) {
773 query.limit_param = Some(self.parse_param_slot("LIMIT")?);
774 query.limit = None;
775 } else {
776 query.limit = Some(self.parse_integer()? as u64);
777 }
778 }
779
780 if self.consume(&Token::Offset)? {
782 if matches!(self.peek(), Token::Dollar | Token::Question) {
783 query.offset_param = Some(self.parse_param_slot("OFFSET")?);
784 query.offset = None;
785 } else {
786 query.offset = Some(self.parse_integer()? as u64);
787 }
788 }
789
790 if self.consume(&Token::With)? && self.consume_ident_ci("EXPAND")? {
792 query.expand = Some(self.parse_expand_options()?);
793 }
794
795 Ok(())
796 }
797
798 fn parse_as_of_spec(&mut self) -> Result<crate::ast::AsOfClause, ParseError> {
806 use crate::ast::AsOfClause;
807
808 let keyword = match self.peek() {
811 Token::Ident(s) => {
812 let s = s.to_ascii_uppercase();
813 self.advance()?;
814 s
815 }
816 Token::Commit => {
817 self.advance()?;
818 "COMMIT".to_string()
819 }
820 other => {
821 return Err(ParseError::expected(
822 vec!["COMMIT", "BRANCH", "TAG", "TIMESTAMP", "SNAPSHOT"],
823 other,
824 self.position(),
825 ));
826 }
827 };
828
829 match keyword.as_str() {
830 "COMMIT" => {
831 let value = self.parse_string()?;
832 Ok(AsOfClause::Commit(value))
833 }
834 "BRANCH" => {
835 let value = self.parse_string()?;
836 Ok(AsOfClause::Branch(value))
837 }
838 "TAG" => {
839 let value = self.parse_string()?;
840 Ok(AsOfClause::Tag(value))
841 }
842 "TIMESTAMP" => {
843 let value = self.parse_integer()?;
844 Ok(AsOfClause::TimestampMs(value))
845 }
846 "SNAPSHOT" => {
847 let value = self.parse_integer()?;
848 if value < 0 {
849 return Err(ParseError::new(
850 "AS OF SNAPSHOT requires non-negative xid".to_string(),
851 self.position(),
852 ));
853 }
854 Ok(AsOfClause::Snapshot(value as u64))
855 }
856 other => Err(ParseError::expected(
857 vec!["COMMIT", "BRANCH", "TAG", "TIMESTAMP", "SNAPSHOT"],
858 &Token::Ident(other.into()),
859 self.position(),
860 )),
861 }
862 }
863
864 fn parse_expand_options(&mut self) -> Result<crate::ast::ExpandOptions, ParseError> {
866 use crate::ast::ExpandOptions;
867 let mut opts = ExpandOptions::default();
868
869 loop {
870 if self.consume(&Token::Graph)? || self.consume_ident_ci("GRAPH")? {
871 opts.graph = true;
872 opts.graph_depth = if self.consume(&Token::Depth)? {
873 self.parse_integer()? as usize
874 } else {
875 1
876 };
877 } else if self.consume_ident_ci("CROSS_REFS")?
878 || self.consume_ident_ci("CROSSREFS")?
879 || self.consume_ident_ci("REFS")?
880 {
881 opts.cross_refs = true;
882 } else if self.consume(&Token::All)? || self.consume_ident_ci("ALL")? {
883 opts.graph = true;
884 opts.cross_refs = true;
885 opts.graph_depth = 1;
886 } else {
887 break;
888 }
889 if !self.consume(&Token::Comma)? {
890 break;
891 }
892 }
893
894 if !opts.graph && !opts.cross_refs {
895 opts.graph = true;
896 opts.cross_refs = true;
897 opts.graph_depth = 1;
898 }
899
900 Ok(opts)
901 }
902
903 pub fn parse_group_by_list(&mut self) -> Result<Vec<String>, ParseError> {
905 Ok(self.parse_group_by_items()?.1)
906 }
907
908 fn parse_group_by_items(&mut self) -> Result<(Vec<Expr>, Vec<String>), ParseError> {
909 let mut exprs = Vec::new();
910 let mut fields = Vec::new();
911 loop {
912 let expr = self.parse_expr()?;
913 let rendered = render_group_by_expr(&expr).ok_or_else(|| {
914 ParseError::new(
915 "GROUP BY expression cannot yet be lowered to legacy runtime representation"
916 .to_string(),
917 self.position(),
918 )
919 })?;
920 exprs.push(expr);
921 fields.push(rendered);
922 if !self.consume(&Token::Comma)? {
923 break;
924 }
925 }
926 Ok((exprs, fields))
927 }
928
929 pub fn parse_order_by_list(&mut self) -> Result<Vec<OrderByClause>, ParseError> {
941 use crate::ast::Expr as AstExpr;
942 let mut clauses = Vec::new();
943 loop {
944 let parsed = self.parse_expr()?;
945 let (field, expr_slot) = match parsed {
946 AstExpr::Column { field, .. } => (field, None),
947 other => (
948 FieldRef::TableColumn {
954 table: String::new(),
955 column: String::new(),
956 },
957 Some(other),
958 ),
959 };
960
961 let ascending = if self.consume(&Token::Desc)? {
962 false
963 } else {
964 self.consume(&Token::Asc)?;
965 true
966 };
967
968 let nulls_first = if self.consume(&Token::Nulls)? {
969 if self.consume(&Token::First)? {
970 true
971 } else {
972 self.expect(Token::Last)?;
973 false
974 }
975 } else {
976 !ascending };
978
979 clauses.push(OrderByClause {
980 field,
981 expr: expr_slot,
982 ascending,
983 nulls_first,
984 });
985
986 if !self.consume(&Token::Comma)? {
987 break;
988 }
989 }
990 Ok(clauses)
991 }
992
993 fn parse_function_literal_arg(&mut self) -> Result<String, ParseError> {
994 let negative = self.consume(&Token::Dash)?;
995 let mut literal = match self.advance()? {
996 Token::Integer(n) => {
997 if negative {
998 format!("-{n}")
999 } else {
1000 n.to_string()
1001 }
1002 }
1003 Token::Float(n) => {
1004 let value = if negative { -n } else { n };
1005 if value.fract().abs() < f64::EPSILON {
1006 format!("{}", value as i64)
1007 } else {
1008 value.to_string()
1009 }
1010 }
1011 other => {
1012 return Err(ParseError::new(
1013 format!("expected number, got {:?}", other),
1019 self.position(),
1020 ));
1021 }
1022 };
1023
1024 if let Token::Ident(unit) = self.peek().clone() {
1025 if is_duration_unit(&unit) {
1026 self.advance()?;
1027 literal.push_str(&unit.to_ascii_lowercase());
1028 }
1029 }
1030
1031 Ok(literal)
1032 }
1033}
1034
1035fn is_duration_unit(unit: &str) -> bool {
1036 matches!(
1037 unit.to_ascii_lowercase().as_str(),
1038 "ms" | "msec"
1039 | "millisecond"
1040 | "milliseconds"
1041 | "s"
1042 | "sec"
1043 | "secs"
1044 | "second"
1045 | "seconds"
1046 | "m"
1047 | "min"
1048 | "mins"
1049 | "minute"
1050 | "minutes"
1051 | "h"
1052 | "hr"
1053 | "hrs"
1054 | "hour"
1055 | "hours"
1056 | "d"
1057 | "day"
1058 | "days"
1059 )
1060}
1061
1062fn render_group_by_expr(expr: &Expr) -> Option<String> {
1063 match expr {
1064 Expr::Column { field, .. } => match field {
1065 FieldRef::TableColumn { table, column } if table.is_empty() => Some(column.clone()),
1066 FieldRef::TableColumn { table, column } => Some(format!("{table}.{column}")),
1067 other => Some(format!("{other:?}")),
1068 },
1069 Expr::FunctionCall { name, args, .. } if name.eq_ignore_ascii_case("TIME_BUCKET") => {
1070 let rendered = args
1071 .iter()
1072 .map(render_group_by_expr)
1073 .collect::<Option<Vec<_>>>()?;
1074 Some(format!("TIME_BUCKET({})", rendered.join(",")))
1075 }
1076 Expr::Literal { value, .. } => Some(match value {
1077 Value::Null => String::new(),
1078 Value::Text(text) => text.to_string(),
1079 other => other.to_string(),
1080 }),
1081 _ => expr_to_projection(expr).map(|projection| match projection {
1082 Projection::Field(FieldRef::TableColumn { table, column }, _) if table.is_empty() => {
1083 column
1084 }
1085 Projection::Field(FieldRef::TableColumn { table, column }, _) => {
1086 format!("{table}.{column}")
1087 }
1088 Projection::Function(name, args) => {
1089 let rendered = args
1090 .iter()
1091 .map(render_group_by_function_arg)
1092 .collect::<Option<Vec<_>>>()
1093 .unwrap_or_default();
1094 format!(
1095 "{}({})",
1096 name.split(':').next().unwrap_or(&name),
1097 rendered.join(",")
1098 )
1099 }
1100 Projection::Column(column) | Projection::Alias(column, _) => column,
1101 Projection::All => "*".to_string(),
1102 Projection::Expression(_, _) => "expr".to_string(),
1103 Projection::Field(other, _) => format!("{other:?}"),
1104 Projection::Window { name, .. } => name,
1105 }),
1106 }
1107}
1108
1109fn render_group_by_function_arg(arg: &Projection) -> Option<String> {
1110 match arg {
1111 Projection::Column(col) => Some(
1112 col.strip_prefix("LIT:")
1113 .map(str::to_string)
1114 .unwrap_or_else(|| col.clone()),
1115 ),
1116 Projection::All => Some("*".to_string()),
1117 _ => None,
1118 }
1119}
1120
1121#[cfg(test)]
1122mod tests {
1123 use super::*;
1124 use crate::ast::{AsOfClause, BinOp, CompareOp, ExpandOptions, TableSource};
1125
1126 fn parse_table(sql: &str) -> TableQuery {
1127 let parsed = crate::parser::parse(sql).unwrap().query;
1128 let QueryExpr::Table(table) = parsed else {
1129 panic!("expected table query");
1130 };
1131 table
1132 }
1133
1134 fn col(name: &str) -> Expr {
1135 Expr::Column {
1136 field: FieldRef::TableColumn {
1137 table: String::new(),
1138 column: name.to_string(),
1139 },
1140 span: Span::synthetic(),
1141 }
1142 }
1143
1144 #[test]
1145 fn helper_function_catalogs_cover_all_names() {
1146 for name in [
1147 "GEO_DISTANCE",
1148 "GEO_DISTANCE_VINCENTY",
1149 "GEO_BEARING",
1150 "GEO_MIDPOINT",
1151 "HAVERSINE",
1152 "VINCENTY",
1153 "TIME_BUCKET",
1154 "UPPER",
1155 "LOWER",
1156 "LENGTH",
1157 "CHAR_LENGTH",
1158 "CHARACTER_LENGTH",
1159 "OCTET_LENGTH",
1160 "BIT_LENGTH",
1161 "SUBSTRING",
1162 "SUBSTR",
1163 "POSITION",
1164 "TRIM",
1165 "LTRIM",
1166 "RTRIM",
1167 "BTRIM",
1168 "CONCAT",
1169 "CONCAT_WS",
1170 "REVERSE",
1171 "LEFT",
1172 "RIGHT",
1173 "QUOTE_LITERAL",
1174 "ABS",
1175 "ROUND",
1176 "COALESCE",
1177 "STDDEV",
1178 "VARIANCE",
1179 "MEDIAN",
1180 "PERCENTILE",
1181 "GROUP_CONCAT",
1182 "STRING_AGG",
1183 "FIRST",
1184 "LAST",
1185 "ARRAY_AGG",
1186 "COUNT_DISTINCT",
1187 "MONEY",
1188 "MONEY_ASSET",
1189 "MONEY_MINOR",
1190 "MONEY_SCALE",
1191 "VERIFY_PASSWORD",
1192 "CAST",
1193 "CASE",
1194 ] {
1195 assert!(is_scalar_function(name), "{name}");
1196 }
1197 assert!(!is_scalar_function("NOT_A_FUNCTION"));
1198
1199 for name in [
1200 "COUNT",
1201 "AVG",
1202 "SUM",
1203 "MIN",
1204 "MAX",
1205 "STDDEV",
1206 "VARIANCE",
1207 "MEDIAN",
1208 "PERCENTILE",
1209 "GROUP_CONCAT",
1210 "STRING_AGG",
1211 "FIRST",
1212 "LAST",
1213 "ARRAY_AGG",
1214 "COUNT_DISTINCT",
1215 ] {
1216 assert!(is_aggregate_function(name), "{name}");
1217 }
1218 assert!(!is_aggregate_function("LOWER"));
1219
1220 assert_eq!(aggregate_token_name(&Token::Count), Some("COUNT"));
1221 assert_eq!(aggregate_token_name(&Token::Sum), Some("SUM"));
1222 assert_eq!(aggregate_token_name(&Token::Avg), Some("AVG"));
1223 assert_eq!(aggregate_token_name(&Token::Min), Some("MIN"));
1224 assert_eq!(aggregate_token_name(&Token::Max), Some("MAX"));
1225 assert_eq!(aggregate_token_name(&Token::First), Some("FIRST"));
1226 assert_eq!(aggregate_token_name(&Token::Last), Some("LAST"));
1227 assert_eq!(aggregate_token_name(&Token::Ident("COUNT".into())), None);
1228
1229 assert_eq!(scalar_token_name(&Token::Left), Some("LEFT"));
1230 assert_eq!(scalar_token_name(&Token::Right), Some("RIGHT"));
1231 assert_eq!(scalar_token_name(&Token::Ident("LEFT".into())), None);
1232
1233 for unit in [
1234 "ms",
1235 "msec",
1236 "millisecond",
1237 "milliseconds",
1238 "s",
1239 "sec",
1240 "secs",
1241 "second",
1242 "seconds",
1243 "m",
1244 "min",
1245 "mins",
1246 "minute",
1247 "minutes",
1248 "h",
1249 "hr",
1250 "hrs",
1251 "hour",
1252 "hours",
1253 "d",
1254 "day",
1255 "days",
1256 ] {
1257 assert!(is_duration_unit(unit), "{unit}");
1258 }
1259 assert!(!is_duration_unit("fortnight"));
1260 }
1261
1262 #[test]
1263 fn projection_and_group_render_helpers_cover_aliases_and_exprs() {
1264 let field = FieldRef::TableColumn {
1265 table: String::new(),
1266 column: "name".into(),
1267 };
1268 let filter = Filter::Compare {
1269 field: field.clone(),
1270 op: CompareOp::Eq,
1271 value: Value::text("alice"),
1272 };
1273
1274 assert_eq!(
1275 attach_projection_alias(Projection::Field(field.clone(), None), Some("n".into())),
1276 Projection::Field(field.clone(), Some("n".into()))
1277 );
1278 assert_eq!(
1279 attach_projection_alias(
1280 Projection::Expression(Box::new(filter.clone()), None),
1281 Some("ok".into())
1282 ),
1283 Projection::Expression(Box::new(filter), Some("ok".into()))
1284 );
1285 assert_eq!(
1286 attach_projection_alias(
1287 Projection::Function("LOWER".into(), vec![]),
1288 Some("l".into())
1289 ),
1290 Projection::Function("LOWER:l".into(), vec![])
1291 );
1292 assert_eq!(
1293 attach_projection_alias(
1294 Projection::Function("LOWER:l".into(), vec![]),
1295 Some("ignored".into())
1296 ),
1297 Projection::Function("LOWER:l".into(), vec![])
1298 );
1299 assert_eq!(
1300 attach_projection_alias(Projection::Column("name".into()), Some("n".into())),
1301 Projection::Alias("name".into(), "n".into())
1302 );
1303 assert_eq!(
1304 attach_projection_alias(Projection::All, Some("ignored".into())),
1305 Projection::All
1306 );
1307
1308 assert_eq!(render_group_by_expr(&col("dept")).as_deref(), Some("dept"));
1309 assert_eq!(
1310 render_group_by_expr(&Expr::Column {
1311 field: FieldRef::TableColumn {
1312 table: "employees".into(),
1313 column: "dept".into()
1314 },
1315 span: Span::synthetic()
1316 })
1317 .as_deref(),
1318 Some("employees.dept")
1319 );
1320 assert_eq!(
1321 render_group_by_expr(&Expr::Column {
1322 field: FieldRef::NodeId { alias: "n".into() },
1323 span: Span::synthetic()
1324 }),
1325 Some("NodeId { alias: \"n\" }".into())
1326 );
1327 assert_eq!(
1328 render_group_by_expr(&Expr::Literal {
1329 value: Value::Null,
1330 span: Span::synthetic()
1331 })
1332 .as_deref(),
1333 Some("")
1334 );
1335 assert_eq!(
1336 render_group_by_expr(&Expr::Literal {
1337 value: Value::text("5m"),
1338 span: Span::synthetic()
1339 })
1340 .as_deref(),
1341 Some("5m")
1342 );
1343 assert_eq!(
1344 render_group_by_expr(&Expr::Literal {
1345 value: Value::Integer(7),
1346 span: Span::synthetic()
1347 })
1348 .as_deref(),
1349 Some("7")
1350 );
1351 assert_eq!(
1352 render_group_by_expr(&Expr::FunctionCall {
1353 name: "TIME_BUCKET".into(),
1354 args: vec![
1355 col("ts"),
1356 Expr::Literal {
1357 value: Value::text("5m"),
1358 span: Span::synthetic()
1359 }
1360 ],
1361 span: Span::synthetic()
1362 })
1363 .as_deref(),
1364 Some("TIME_BUCKET(ts,5m)")
1365 );
1366 assert_eq!(
1367 render_group_by_expr(&Expr::FunctionCall {
1368 name: "LOWER".into(),
1369 args: vec![col("dept")],
1370 span: Span::synthetic()
1371 })
1372 .as_deref(),
1373 Some("LOWER()")
1374 );
1375
1376 assert_eq!(
1377 render_group_by_function_arg(&Projection::Column("LIT:5m".into())),
1378 Some("5m".into())
1379 );
1380 assert_eq!(
1381 render_group_by_function_arg(&Projection::Column("dept".into())),
1382 Some("dept".into())
1383 );
1384 assert_eq!(
1385 render_group_by_function_arg(&Projection::All),
1386 Some("*".into())
1387 );
1388 assert_eq!(
1389 render_group_by_function_arg(&Projection::Function("LOWER".into(), vec![])),
1390 None
1391 );
1392 }
1393
1394 #[test]
1395 fn expression_aggregate_detection_branches() {
1396 let count = Expr::FunctionCall {
1397 name: "COUNT".into(),
1398 args: vec![col("id")],
1399 span: Span::synthetic(),
1400 };
1401 assert!(contains_nested_aggregate(&count));
1402 assert!(is_plain_aggregate_expr(&count));
1403
1404 let nested = Expr::FunctionCall {
1405 name: "SUM".into(),
1406 args: vec![count.clone()],
1407 span: Span::synthetic(),
1408 };
1409 assert!(contains_nested_aggregate(&nested));
1410 assert!(!is_plain_aggregate_expr(&nested));
1411
1412 let binary = Expr::BinaryOp {
1413 op: BinOp::Add,
1414 lhs: Box::new(col("a")),
1415 rhs: Box::new(count.clone()),
1416 span: Span::synthetic(),
1417 };
1418 assert!(contains_nested_aggregate(&binary));
1419
1420 let unary = Expr::UnaryOp {
1421 op: UnaryOp::Not,
1422 operand: Box::new(count.clone()),
1423 span: Span::synthetic(),
1424 };
1425 assert!(contains_nested_aggregate(&unary));
1426
1427 let cast = Expr::Cast {
1428 inner: Box::new(count.clone()),
1429 target: reddb_types::types::DataType::Integer,
1430 span: Span::synthetic(),
1431 };
1432 assert!(contains_nested_aggregate(&cast));
1433
1434 let case = Expr::Case {
1435 branches: vec![(col("flag"), count.clone())],
1436 else_: Some(Box::new(col("fallback"))),
1437 span: Span::synthetic(),
1438 };
1439 assert!(contains_nested_aggregate(&case));
1440
1441 let in_list = Expr::InList {
1442 target: Box::new(col("id")),
1443 values: vec![count.clone()],
1444 negated: false,
1445 span: Span::synthetic(),
1446 };
1447 assert!(contains_nested_aggregate(&in_list));
1448
1449 let between = Expr::Between {
1450 target: Box::new(col("id")),
1451 low: Box::new(col("low")),
1452 high: Box::new(count),
1453 negated: false,
1454 span: Span::synthetic(),
1455 };
1456 assert!(contains_nested_aggregate(&between));
1457 assert!(!contains_nested_aggregate(&Expr::Parameter {
1458 index: 1,
1459 span: Span::synthetic()
1460 }));
1461
1462 assert!(crate::parser::parse("SELECT SUM(COUNT(id)) FROM t").is_err());
1463 }
1464
1465 #[test]
1466 fn table_clause_parsing_covers_as_of_order_offset_and_expand() {
1467 let table = parse_table(
1468 "SELECT name FROM users AS OF COMMIT 'abc123' \
1469 WHERE deleted_at IS NULL \
1470 ORDER BY LOWER(name) ASC NULLS FIRST, created_at DESC NULLS LAST \
1471 LIMIT 10 OFFSET 5 WITH EXPAND GRAPH DEPTH 3, CROSS_REFS",
1472 );
1473 assert!(matches!(table.as_of, Some(AsOfClause::Commit(ref v)) if v == "abc123"));
1474 assert!(table.filter.is_some());
1475 assert_eq!(table.order_by.len(), 2);
1476 assert!(table.order_by[0].expr.is_some());
1477 assert!(table.order_by[0].ascending);
1478 assert!(table.order_by[0].nulls_first);
1479 assert!(!table.order_by[1].ascending);
1480 assert!(!table.order_by[1].nulls_first);
1481 assert_eq!(table.limit, Some(10));
1482 assert_eq!(table.offset, Some(5));
1483 assert!(matches!(
1484 table.expand,
1485 Some(ExpandOptions {
1486 graph: true,
1487 graph_depth: 3,
1488 cross_refs: true,
1489 ..
1490 })
1491 ));
1492
1493 let table = parse_table("SELECT * FROM users AS OF BRANCH 'main'");
1494 assert!(matches!(table.as_of, Some(AsOfClause::Branch(ref v)) if v == "main"));
1495
1496 let table = parse_table("SELECT * FROM users AS OF TAG 'v1'");
1497 assert!(matches!(table.as_of, Some(AsOfClause::Tag(ref v)) if v == "v1"));
1498
1499 let table = parse_table("SELECT * FROM users AS OF TIMESTAMP 1710000000000");
1500 assert!(matches!(
1501 table.as_of,
1502 Some(AsOfClause::TimestampMs(1_710_000_000_000))
1503 ));
1504
1505 let table = parse_table("SELECT * FROM users AS OF SNAPSHOT 42");
1506 assert!(matches!(table.as_of, Some(AsOfClause::Snapshot(42))));
1507
1508 let table = parse_table("SELECT * FROM users WITH EXPAND");
1509 assert!(matches!(
1510 table.expand,
1511 Some(ExpandOptions {
1512 graph: true,
1513 graph_depth: 1,
1514 cross_refs: true,
1515 ..
1516 })
1517 ));
1518
1519 assert!(crate::parser::parse("SELECT * FROM users AS OF SNAPSHOT -1").is_err());
1520 assert!(crate::parser::parse("SELECT * FROM users AS OF UNKNOWN 'x'").is_err());
1521 }
1522
1523 #[test]
1524 fn direct_parser_helpers_cover_projection_group_order_and_literals() {
1525 let mut parser = Parser::new("name, LOWER(email) AS email_l").unwrap();
1526 let projections = parser.parse_projection_list().unwrap();
1527 assert_eq!(projections.len(), 2);
1528
1529 let mut parser = Parser::new("dept, TIME_BUCKET(5 m)").unwrap();
1530 let group_by = parser.parse_group_by_list().unwrap();
1531 assert_eq!(group_by, vec!["dept", "TIME_BUCKET(5m)"]);
1532
1533 let mut parser = Parser::new("LOWER(name) DESC, created_at").unwrap();
1534 let order_by = parser.parse_order_by_list().unwrap();
1535 assert_eq!(order_by.len(), 2);
1536 assert!(order_by[0].expr.is_some());
1537 assert!(!order_by[0].ascending);
1538 assert!(order_by[0].nulls_first);
1539 assert!(order_by[1].ascending);
1540 assert!(!order_by[1].nulls_first);
1541
1542 let mut parser = Parser::new("-5 ms").unwrap();
1543 assert_eq!(parser.parse_function_literal_arg().unwrap(), "-5ms");
1544 let mut parser = Parser::new("2.0 H").unwrap();
1545 assert_eq!(parser.parse_function_literal_arg().unwrap(), "2h");
1546 let mut parser = Parser::new("bad").unwrap();
1547 assert!(parser.parse_function_literal_arg().is_err());
1548 }
1549
1550 #[test]
1551 fn from_subquery_source_is_preserved() {
1552 let parsed = crate::parser::parse("FROM (SELECT id FROM users) AS u RETURN u.id")
1553 .unwrap()
1554 .query;
1555 let QueryExpr::Table(table) = parsed else {
1556 panic!("expected table query");
1557 };
1558 assert_eq!(table.table, "__subq_u");
1559 assert_eq!(table.alias.as_deref(), Some("u"));
1560 assert!(matches!(table.source, Some(TableSource::Subquery(_))));
1561 assert_eq!(table.select_items.len(), 1);
1562
1563 let parsed = crate::parser::parse("SELECT id FROM (SELECT id FROM users) AS u")
1564 .unwrap()
1565 .query;
1566 let QueryExpr::Table(table) = parsed else {
1567 panic!("expected table query");
1568 };
1569 assert_eq!(table.table, "__subq_u");
1570 assert_eq!(table.alias.as_deref(), Some("u"));
1571 assert!(matches!(table.source, Some(TableSource::Subquery(_))));
1572 assert_eq!(table.select_items.len(), 1);
1573
1574 assert!(crate::parser::parse("FROM (MATCH (n) RETURN n) AS g").is_err());
1575 assert!(crate::parser::parse("SELECT * FROM (MATCH (n) RETURN n) AS g").is_err());
1576 }
1577
1578 #[test]
1581 fn louvain_tvf_parses_positional_and_named_args() {
1582 let table = parse_table("SELECT * FROM louvain(g)");
1584 match table.source {
1585 Some(TableSource::Function {
1586 ref name,
1587 ref args,
1588 ref named_args,
1589 }) => {
1590 assert_eq!(name, "louvain");
1591 assert_eq!(args, &vec!["g".to_string()]);
1592 assert!(named_args.is_empty());
1593 }
1594 other => panic!("expected louvain TVF source, got {other:?}"),
1595 }
1596
1597 let table = parse_table("SELECT * FROM louvain(g, resolution => 0.5)");
1599 match table.source {
1600 Some(TableSource::Function {
1601 ref name,
1602 ref args,
1603 ref named_args,
1604 }) => {
1605 assert_eq!(name, "louvain");
1606 assert_eq!(args, &vec!["g".to_string()]);
1607 assert_eq!(named_args.len(), 1);
1608 assert_eq!(named_args[0].0, "resolution");
1609 assert!((named_args[0].1 - 0.5).abs() < f64::EPSILON);
1610 }
1611 other => panic!("expected louvain TVF source, got {other:?}"),
1612 }
1613
1614 let table = parse_table("SELECT * FROM louvain(g, resolution => 2)");
1616 match table.source {
1617 Some(TableSource::Function { ref named_args, .. }) => {
1618 assert!((named_args[0].1 - 2.0).abs() < f64::EPSILON);
1619 }
1620 other => panic!("expected louvain TVF source, got {other:?}"),
1621 }
1622 }
1623
1624 #[test]
1627 fn tvf_inline_form_parses_nodes_and_edges_subqueries() {
1628 let table = parse_table(
1631 "SELECT * FROM components(nodes => (SELECT id FROM hosts), edges => (SELECT src, dst FROM links))",
1632 );
1633 match table.source {
1634 Some(TableSource::InlineGraphFunction {
1635 ref name,
1636 ref nodes,
1637 ref edges,
1638 ref named_args,
1639 }) => {
1640 assert_eq!(name, "components");
1641 assert!(named_args.is_empty());
1642 assert!(matches!(**nodes, QueryExpr::Table(_)));
1643 assert!(matches!(**edges, QueryExpr::Table(_)));
1644 }
1645 other => panic!("expected inline graph TVF source, got {other:?}"),
1646 }
1647 }
1648
1649 #[test]
1650 fn tvf_inline_form_carries_numeric_named_args() {
1651 let table = parse_table(
1653 "SELECT * FROM louvain(nodes => (SELECT id FROM n), edges => (SELECT a, b FROM e), resolution => 0.5)",
1654 );
1655 match table.source {
1656 Some(TableSource::InlineGraphFunction {
1657 ref name,
1658 ref named_args,
1659 ..
1660 }) => {
1661 assert_eq!(name, "louvain");
1662 assert_eq!(named_args.len(), 1);
1663 assert_eq!(named_args[0].0, "resolution");
1664 assert!((named_args[0].1 - 0.5).abs() < f64::EPSILON);
1665 }
1666 other => panic!("expected inline graph TVF source, got {other:?}"),
1667 }
1668 }
1669
1670 #[test]
1671 fn tvf_inline_form_rejects_malformed_shapes() {
1672 assert!(crate::parser::parse(
1674 "SELECT * FROM components(g, nodes => (SELECT id FROM n), edges => (SELECT a, b FROM e))"
1675 )
1676 .is_err());
1677 assert!(
1679 crate::parser::parse("SELECT * FROM components(nodes => (SELECT id FROM n))").is_err()
1680 );
1681 assert!(
1682 crate::parser::parse("SELECT * FROM components(edges => (SELECT a, b FROM e))")
1683 .is_err()
1684 );
1685 assert!(crate::parser::parse(
1687 "SELECT * FROM components(nodes => (SELECT id FROM n), verts => (SELECT a, b FROM e))"
1688 )
1689 .is_err());
1690 assert!(crate::parser::parse(
1692 "SELECT * FROM components(nodes => (1 + 2), edges => (SELECT a, b FROM e))"
1693 )
1694 .is_err());
1695 }
1696
1697 #[test]
1698 fn shortest_path_tvf_parses_graph_ref_with_scalar_named_args() {
1699 let table = parse_table("SELECT * FROM shortest_path(g, src => 1, dst => 4)");
1701 match table.source {
1702 Some(TableSource::Function {
1703 ref name,
1704 ref args,
1705 ref named_args,
1706 }) => {
1707 assert_eq!(name, "shortest_path");
1708 assert_eq!(args, &vec!["g".to_string()]);
1709 assert_eq!(named_args.len(), 2);
1710 assert_eq!(named_args[0].0, "src");
1711 assert!((named_args[0].1 - 1.0).abs() < f64::EPSILON);
1712 assert_eq!(named_args[1].0, "dst");
1713 assert!((named_args[1].1 - 4.0).abs() < f64::EPSILON);
1714 }
1715 other => panic!("expected shortest_path TVF source, got {other:?}"),
1716 }
1717
1718 let table =
1720 parse_table("SELECT * FROM shortest_path(g, src => 1, dst => 4, max_hops => 3)");
1721 match table.source {
1722 Some(TableSource::Function { ref named_args, .. }) => {
1723 assert_eq!(named_args.len(), 3);
1724 assert_eq!(named_args[2].0, "max_hops");
1725 assert!((named_args[2].1 - 3.0).abs() < f64::EPSILON);
1726 }
1727 other => panic!("expected shortest_path TVF source, got {other:?}"),
1728 }
1729 }
1730
1731 #[test]
1732 fn centrality_tvfs_parse_positional_and_named_args() {
1733 for name in ["betweenness", "eigenvector", "pagerank"] {
1737 let table = parse_table(&format!("SELECT * FROM {name}(g)"));
1738 match table.source {
1739 Some(TableSource::Function {
1740 name: ref got,
1741 ref args,
1742 ref named_args,
1743 }) => {
1744 assert_eq!(got, name);
1745 assert_eq!(args, &vec!["g".to_string()]);
1746 assert!(named_args.is_empty());
1747 }
1748 other => panic!("expected {name} TVF source, got {other:?}"),
1749 }
1750 }
1751
1752 let table =
1754 parse_table("SELECT * FROM eigenvector(g, max_iterations => 50, tolerance => 0.0001)");
1755 match table.source {
1756 Some(TableSource::Function { ref named_args, .. }) => {
1757 assert_eq!(named_args.len(), 2);
1758 assert_eq!(named_args[0].0, "max_iterations");
1759 assert!((named_args[0].1 - 50.0).abs() < f64::EPSILON);
1760 assert_eq!(named_args[1].0, "tolerance");
1761 assert!((named_args[1].1 - 0.0001).abs() < f64::EPSILON);
1762 }
1763 other => panic!("expected eigenvector TVF source, got {other:?}"),
1764 }
1765
1766 let table =
1768 parse_table("SELECT * FROM pagerank(g, damping => 0.85, max_iterations => 100)");
1769 match table.source {
1770 Some(TableSource::Function {
1771 ref args,
1772 ref named_args,
1773 ..
1774 }) => {
1775 assert_eq!(args, &vec!["g".to_string()]);
1776 assert_eq!(named_args.len(), 2);
1777 assert_eq!(named_args[0].0, "damping");
1778 assert!((named_args[0].1 - 0.85).abs() < f64::EPSILON);
1779 assert_eq!(named_args[1].0, "max_iterations");
1780 assert!((named_args[1].1 - 100.0).abs() < f64::EPSILON);
1781 }
1782 other => panic!("expected pagerank TVF source, got {other:?}"),
1783 }
1784 }
1785
1786 #[test]
1787 fn tvf_named_arg_grammar_rejects_malformed_forms() {
1788 assert!(crate::parser::parse("SELECT * FROM louvain(g, resolution => 0.5, h)").is_err());
1790 assert!(crate::parser::parse("SELECT * FROM louvain(g, resolution => foo)").is_err());
1792 assert!(crate::parser::parse("SELECT * FROM louvain()").is_err());
1794 }
1795
1796 #[test]
1799 fn test_parse_sessionize_full_clause() {
1800 let q = parse_table(
1801 "SELECT user_id, ts FROM events SESSIONIZE BY user_id GAP 30 m ORDER BY ts",
1802 );
1803 let s = q.sessionize.expect("sessionize present");
1804 assert_eq!(s.actor_col.as_deref(), Some("user_id"));
1805 assert_eq!(s.gap_ms, Some(30 * 60_000));
1806 assert_eq!(s.order_col.as_deref(), Some("ts"));
1807 }
1808
1809 #[test]
1810 fn test_parse_sessionize_omits_optional_order_by() {
1811 let q = parse_table("SELECT * FROM events SESSIONIZE BY user_id GAP 5 s");
1812 let s = q.sessionize.expect("sessionize present");
1813 assert_eq!(s.actor_col.as_deref(), Some("user_id"));
1814 assert_eq!(s.gap_ms, Some(5_000));
1815 assert!(s.order_col.is_none());
1816 }
1817
1818 #[test]
1819 fn test_parse_sessionize_bare_defers_to_descriptor() {
1820 let q = parse_table("SELECT * FROM events SESSIONIZE");
1824 let s = q.sessionize.expect("sessionize present");
1825 assert!(s.actor_col.is_none());
1826 assert!(s.gap_ms.is_none());
1827 assert!(s.order_col.is_none());
1828 }
1829
1830 #[test]
1831 fn test_parse_sessionize_composes_with_where_and_limit() {
1832 let q = parse_table(
1833 "SELECT user_id FROM events \
1834 SESSIONIZE BY user_id GAP 1 m \
1835 WHERE user_id = 'u1' LIMIT 10",
1836 );
1837 let s = q.sessionize.expect("sessionize present");
1838 assert_eq!(s.actor_col.as_deref(), Some("user_id"));
1839 assert_eq!(s.gap_ms, Some(60_000));
1840 assert!(q.where_expr.is_some(), "WHERE still parsed");
1841 assert_eq!(q.limit, Some(10));
1842 }
1843
1844 #[test]
1845 fn test_parse_sessionize_absent_leaves_field_none() {
1846 let q = parse_table("SELECT * FROM events");
1847 assert!(q.sessionize.is_none());
1848 }
1849
1850 #[test]
1851 fn test_parse_sessionize_with_session_id_in_projection_e2e_shape() {
1852 let q = parse_table(
1855 "SELECT id, user_id, ts, session_id FROM events \
1856 SESSIONIZE BY user_id GAP 30 s ORDER BY ts",
1857 );
1858 let s = q.sessionize.expect("sessionize present");
1859 assert_eq!(s.actor_col.as_deref(), Some("user_id"));
1860 assert_eq!(s.gap_ms, Some(30_000));
1861 }
1862}