1use super::super::ast::{
4 BinOp, CompareOp, Expr, FieldRef, Filter, OrderByClause, Projection, QueryExpr,
5 QueueSelectQuery, SelectItem, Span, TableQuery, UnaryOp,
6};
7use super::super::lexer::Token;
8use super::error::ParseError;
9use crate::storage::query::sql_lowering::{expr_to_projection, filter_to_expr};
10use crate::storage::schema::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 fn parse_select_query_inner(&mut self) -> Result<QueryExpr, ParseError> {
122 self.expect(Token::Select)?;
123
124 let (select_items, columns) = self.parse_select_items_and_projections()?;
126
127 let has_from = self.consume(&Token::From)?;
130 let table = if has_from {
131 if self.consume(&Token::Queue)? {
132 let queue = self.expect_ident()?;
133 let filter = if self.consume(&Token::Where)? {
134 Some(self.parse_filter()?)
135 } else {
136 None
137 };
138 let limit = if self.consume(&Token::Limit)? {
139 Some(self.parse_integer()? as u64)
140 } else {
141 None
142 };
143 return Ok(QueryExpr::QueueSelect(QueueSelectQuery {
144 queue,
145 columns: queue_projection_columns(&columns)?,
146 filter,
147 limit,
148 }));
149 } else if self.consume(&Token::Star)? {
150 "*".to_string()
151 } else if self.consume(&Token::All)? {
152 "all".to_string()
153 } else {
154 self.expect_ident()?
155 }
156 } else {
157 "any".to_string()
158 };
159
160 let alias =
164 if !has_from || (self.check(&Token::As) && matches!(self.peek_next()?, Token::Of)) {
165 None
166 } else if self.consume(&Token::As)?
167 || (self.check(&Token::Ident("".into())) && !self.is_clause_keyword())
168 {
169 Some(self.expect_ident()?)
170 } else {
171 None
172 };
173
174 let mut query = TableQuery {
175 table,
176 source: None,
177 alias,
178 select_items,
179 columns,
180 where_expr: None,
181 filter: None,
182 group_by_exprs: Vec::new(),
183 group_by: Vec::new(),
184 having_expr: None,
185 having: None,
186 order_by: Vec::new(),
187 limit: None,
188 limit_param: None,
189 offset: None,
190 offset_param: None,
191 expand: None,
192 as_of: None,
193 };
194
195 if self.is_join_keyword() {
196 let return_items = std::mem::take(&mut query.select_items);
197 let return_ = std::mem::take(&mut query.columns);
198 let mut expr = self.parse_join_query(QueryExpr::Table(query))?;
199 if let QueryExpr::Join(join) = &mut expr {
200 join.return_items = return_items;
201 join.return_ = return_;
202 }
203 return Ok(expr);
204 }
205
206 self.parse_table_clauses(&mut query)?;
208
209 Ok(QueryExpr::Table(query))
210 }
211}
212
213impl<'a> Parser<'a> {
214 pub fn is_clause_keyword(&self) -> bool {
216 matches!(
217 self.peek(),
218 Token::Where
219 | Token::Order
220 | Token::Limit
221 | Token::Offset
222 | Token::Join
223 | Token::Inner
224 | Token::Left
225 | Token::Right
226 | Token::As
227 )
228 }
229
230 pub fn parse_projection_list(&mut self) -> Result<Vec<Projection>, ParseError> {
232 Ok(self.parse_select_items_and_projections()?.1)
233 }
234
235 pub(crate) fn parse_select_items_and_projections(
236 &mut self,
237 ) -> Result<(Vec<SelectItem>, Vec<Projection>), ParseError> {
238 if self.consume(&Token::Star)? {
240 return Ok((vec![SelectItem::Wildcard], Vec::new())); }
242
243 let mut select_items = Vec::new();
244 let mut projections = Vec::new();
245 loop {
246 let (item, proj) = self.parse_projection()?;
247 select_items.push(item);
248 projections.push(proj);
249
250 if !self.consume(&Token::Comma)? {
251 break;
252 }
253 }
254 Ok((select_items, projections))
255 }
256
257 fn parse_projection(&mut self) -> Result<(SelectItem, Projection), ParseError> {
259 let expr = self.parse_expr()?;
260 if contains_nested_aggregate(&expr) && !is_plain_aggregate_expr(&expr) {
261 return Err(ParseError::new(
262 "aggregate function is not valid inside another expression".to_string(),
263 self.position(),
264 ));
265 }
266 let alias = if self.consume(&Token::As)? {
267 Some(self.expect_ident()?)
268 } else {
269 None
270 };
271 let select_item = SelectItem::Expr {
272 expr: expr.clone(),
273 alias: alias.clone(),
274 };
275 let projection = attach_projection_alias(
276 expr_to_projection(&expr).ok_or_else(|| {
277 ParseError::new(
278 "projection cannot yet be lowered to legacy runtime representation".to_string(),
279 self.position(),
280 )
281 })?,
282 alias,
283 );
284 Ok((select_item, projection))
285 }
286}
287
288fn contains_nested_aggregate(expr: &Expr) -> bool {
289 match expr {
290 Expr::FunctionCall { name, args, .. } => {
291 is_aggregate_function(&name.to_uppercase())
292 || args.iter().any(contains_nested_aggregate)
293 }
294 Expr::BinaryOp { lhs, rhs, .. } => {
295 contains_nested_aggregate(lhs) || contains_nested_aggregate(rhs)
296 }
297 Expr::UnaryOp { operand, .. } | Expr::IsNull { operand, .. } => {
298 contains_nested_aggregate(operand)
299 }
300 Expr::Cast { inner, .. } => contains_nested_aggregate(inner),
301 Expr::Case {
302 branches, else_, ..
303 } => {
304 branches.iter().any(|(cond, value)| {
305 contains_nested_aggregate(cond) || contains_nested_aggregate(value)
306 }) || else_.as_deref().is_some_and(contains_nested_aggregate)
307 }
308 Expr::InList { target, values, .. } => {
309 contains_nested_aggregate(target) || values.iter().any(contains_nested_aggregate)
310 }
311 Expr::Between {
312 target, low, high, ..
313 } => {
314 contains_nested_aggregate(target)
315 || contains_nested_aggregate(low)
316 || contains_nested_aggregate(high)
317 }
318 Expr::Literal { .. }
319 | Expr::Column { .. }
320 | Expr::Parameter { .. }
321 | Expr::Subquery { .. } => false,
322 }
323}
324
325fn is_plain_aggregate_expr(expr: &Expr) -> bool {
326 match expr {
327 Expr::FunctionCall { name, args, .. } if is_aggregate_function(&name.to_uppercase()) => {
328 !args.iter().any(contains_nested_aggregate)
329 }
330 _ => false,
331 }
332}
333
334fn attach_projection_alias(proj: Projection, alias: Option<String>) -> Projection {
335 let Some(alias) = alias else { return proj };
336 match proj {
337 Projection::Field(field, _) => Projection::Field(field, Some(alias)),
338 Projection::Expression(filter, _) => Projection::Expression(filter, Some(alias)),
339 Projection::Function(name, args) => {
340 if name.contains(':') {
341 Projection::Function(name, args)
342 } else {
343 Projection::Function(format!("{name}:{alias}"), args)
344 }
345 }
346 Projection::Column(column) => Projection::Alias(column, alias),
347 other => other,
348 }
349}
350
351fn queue_projection_columns(columns: &[Projection]) -> Result<Vec<String>, ParseError> {
352 let mut out = Vec::new();
353 for column in columns {
354 match column {
355 Projection::Column(name) => out.push(name.clone()),
356 Projection::Alias(name, _) => out.push(name.clone()),
357 Projection::Field(FieldRef::TableColumn { table, column }, _) if table.is_empty() => {
358 out.push(column.clone());
359 }
360 Projection::All => return Ok(Vec::new()),
361 other => {
362 return Err(ParseError::new(
363 format!("unsupported SELECT FROM QUEUE projection {other:?}"),
364 crate::storage::query::lexer::Position::default(),
365 ));
366 }
367 }
368 }
369 Ok(out)
370}
371
372impl<'a> Parser<'a> {
373 pub fn parse_table_clauses(&mut self, query: &mut TableQuery) -> Result<(), ParseError> {
375 if self.check(&Token::As) {
378 let next_is_of = matches!(self.peek_next()?, Token::Of);
379 if next_is_of {
380 self.expect(Token::As)?;
381 self.expect(Token::Of)?;
382 query.as_of = Some(self.parse_as_of_spec()?);
383 }
384 }
385
386 if self.consume(&Token::Where)? {
388 let filter = self.parse_filter()?;
389 query.where_expr = Some(filter_to_expr(&filter));
390 query.filter = Some(filter);
391 }
392
393 if self.consume(&Token::Group)? {
395 self.expect(Token::By)?;
396 let (group_by_exprs, group_by) = self.parse_group_by_items()?;
397 query.group_by_exprs = group_by_exprs;
398 query.group_by = group_by;
399 }
400
401 if !query.group_by_exprs.is_empty() && self.consume_ident_ci("HAVING")? {
403 let having = self.parse_filter()?;
404 query.having_expr = Some(filter_to_expr(&having));
405 query.having = Some(having);
406 }
407
408 if self.consume(&Token::Order)? {
410 self.expect(Token::By)?;
411 query.order_by = self.parse_order_by_list()?;
412 }
413
414 if self.consume(&Token::Limit)? {
416 if matches!(self.peek(), Token::Dollar | Token::Question) {
417 query.limit_param = Some(self.parse_param_slot("LIMIT")?);
418 query.limit = None;
419 } else {
420 query.limit = Some(self.parse_integer()? as u64);
421 }
422 }
423
424 if self.consume(&Token::Offset)? {
426 if matches!(self.peek(), Token::Dollar | Token::Question) {
427 query.offset_param = Some(self.parse_param_slot("OFFSET")?);
428 query.offset = None;
429 } else {
430 query.offset = Some(self.parse_integer()? as u64);
431 }
432 }
433
434 if self.consume(&Token::With)? && self.consume_ident_ci("EXPAND")? {
436 query.expand = Some(self.parse_expand_options()?);
437 }
438
439 Ok(())
440 }
441
442 fn parse_as_of_spec(&mut self) -> Result<crate::storage::query::ast::AsOfClause, ParseError> {
450 use crate::storage::query::ast::AsOfClause;
451
452 let keyword = match self.peek() {
455 Token::Ident(s) => {
456 let s = s.to_ascii_uppercase();
457 self.advance()?;
458 s
459 }
460 Token::Commit => {
461 self.advance()?;
462 "COMMIT".to_string()
463 }
464 other => {
465 return Err(ParseError::expected(
466 vec!["COMMIT", "BRANCH", "TAG", "TIMESTAMP", "SNAPSHOT"],
467 other,
468 self.position(),
469 ));
470 }
471 };
472
473 match keyword.as_str() {
474 "COMMIT" => {
475 let value = self.parse_string()?;
476 Ok(AsOfClause::Commit(value))
477 }
478 "BRANCH" => {
479 let value = self.parse_string()?;
480 Ok(AsOfClause::Branch(value))
481 }
482 "TAG" => {
483 let value = self.parse_string()?;
484 Ok(AsOfClause::Tag(value))
485 }
486 "TIMESTAMP" => {
487 let value = self.parse_integer()?;
488 Ok(AsOfClause::TimestampMs(value))
489 }
490 "SNAPSHOT" => {
491 let value = self.parse_integer()?;
492 if value < 0 {
493 return Err(ParseError::new(
494 "AS OF SNAPSHOT requires non-negative xid".to_string(),
495 self.position(),
496 ));
497 }
498 Ok(AsOfClause::Snapshot(value as u64))
499 }
500 other => Err(ParseError::expected(
501 vec!["COMMIT", "BRANCH", "TAG", "TIMESTAMP", "SNAPSHOT"],
502 &Token::Ident(other.into()),
503 self.position(),
504 )),
505 }
506 }
507
508 fn parse_expand_options(
510 &mut self,
511 ) -> Result<crate::storage::query::ast::ExpandOptions, ParseError> {
512 use crate::storage::query::ast::ExpandOptions;
513 let mut opts = ExpandOptions::default();
514
515 loop {
516 if self.consume(&Token::Graph)? || self.consume_ident_ci("GRAPH")? {
517 opts.graph = true;
518 opts.graph_depth = if self.consume(&Token::Depth)? {
519 self.parse_integer()? as usize
520 } else {
521 1
522 };
523 } else if self.consume_ident_ci("CROSS_REFS")?
524 || self.consume_ident_ci("CROSSREFS")?
525 || self.consume_ident_ci("REFS")?
526 {
527 opts.cross_refs = true;
528 } else if self.consume(&Token::All)? || self.consume_ident_ci("ALL")? {
529 opts.graph = true;
530 opts.cross_refs = true;
531 opts.graph_depth = 1;
532 } else {
533 break;
534 }
535 if !self.consume(&Token::Comma)? {
536 break;
537 }
538 }
539
540 if !opts.graph && !opts.cross_refs {
541 opts.graph = true;
542 opts.cross_refs = true;
543 opts.graph_depth = 1;
544 }
545
546 Ok(opts)
547 }
548
549 pub fn parse_group_by_list(&mut self) -> Result<Vec<String>, ParseError> {
551 Ok(self.parse_group_by_items()?.1)
552 }
553
554 fn parse_group_by_items(&mut self) -> Result<(Vec<Expr>, Vec<String>), ParseError> {
555 let mut exprs = Vec::new();
556 let mut fields = Vec::new();
557 loop {
558 let expr = self.parse_expr()?;
559 let rendered = render_group_by_expr(&expr).ok_or_else(|| {
560 ParseError::new(
561 "GROUP BY expression cannot yet be lowered to legacy runtime representation"
562 .to_string(),
563 self.position(),
564 )
565 })?;
566 exprs.push(expr);
567 fields.push(rendered);
568 if !self.consume(&Token::Comma)? {
569 break;
570 }
571 }
572 Ok((exprs, fields))
573 }
574
575 pub fn parse_order_by_list(&mut self) -> Result<Vec<OrderByClause>, ParseError> {
587 use super::super::ast::Expr as AstExpr;
588 let mut clauses = Vec::new();
589 loop {
590 let parsed = self.parse_expr()?;
591 let (field, expr_slot) = match parsed {
592 AstExpr::Column { field, .. } => (field, None),
593 other => (
594 FieldRef::TableColumn {
600 table: String::new(),
601 column: String::new(),
602 },
603 Some(other),
604 ),
605 };
606
607 let ascending = if self.consume(&Token::Desc)? {
608 false
609 } else {
610 self.consume(&Token::Asc)?;
611 true
612 };
613
614 let nulls_first = if self.consume(&Token::Nulls)? {
615 if self.consume(&Token::First)? {
616 true
617 } else {
618 self.expect(Token::Last)?;
619 false
620 }
621 } else {
622 !ascending };
624
625 clauses.push(OrderByClause {
626 field,
627 expr: expr_slot,
628 ascending,
629 nulls_first,
630 });
631
632 if !self.consume(&Token::Comma)? {
633 break;
634 }
635 }
636 Ok(clauses)
637 }
638
639 fn parse_function_literal_arg(&mut self) -> Result<String, ParseError> {
640 let negative = self.consume(&Token::Dash)?;
641 let mut literal = match self.advance()? {
642 Token::Integer(n) => {
643 if negative {
644 format!("-{n}")
645 } else {
646 n.to_string()
647 }
648 }
649 Token::Float(n) => {
650 let value = if negative { -n } else { n };
651 if value.fract().abs() < f64::EPSILON {
652 format!("{}", value as i64)
653 } else {
654 value.to_string()
655 }
656 }
657 other => {
658 return Err(ParseError::new(
659 format!("expected number, got {:?}", other),
665 self.position(),
666 ));
667 }
668 };
669
670 if let Token::Ident(unit) = self.peek().clone() {
671 if is_duration_unit(&unit) {
672 self.advance()?;
673 literal.push_str(&unit.to_ascii_lowercase());
674 }
675 }
676
677 Ok(literal)
678 }
679}
680
681fn is_duration_unit(unit: &str) -> bool {
682 matches!(
683 unit.to_ascii_lowercase().as_str(),
684 "ms" | "msec"
685 | "millisecond"
686 | "milliseconds"
687 | "s"
688 | "sec"
689 | "secs"
690 | "second"
691 | "seconds"
692 | "m"
693 | "min"
694 | "mins"
695 | "minute"
696 | "minutes"
697 | "h"
698 | "hr"
699 | "hrs"
700 | "hour"
701 | "hours"
702 | "d"
703 | "day"
704 | "days"
705 )
706}
707
708fn render_group_by_expr(expr: &Expr) -> Option<String> {
709 match expr {
710 Expr::Column { field, .. } => match field {
711 FieldRef::TableColumn { table, column } if table.is_empty() => Some(column.clone()),
712 FieldRef::TableColumn { table, column } => Some(format!("{table}.{column}")),
713 other => Some(format!("{other:?}")),
714 },
715 Expr::FunctionCall { name, args, .. } if name.eq_ignore_ascii_case("TIME_BUCKET") => {
716 let rendered = args
717 .iter()
718 .map(render_group_by_expr)
719 .collect::<Option<Vec<_>>>()?;
720 Some(format!("TIME_BUCKET({})", rendered.join(",")))
721 }
722 Expr::Literal { value, .. } => Some(match value {
723 Value::Null => String::new(),
724 Value::Text(text) => text.to_string(),
725 other => other.to_string(),
726 }),
727 _ => expr_to_projection(expr).map(|projection| match projection {
728 Projection::Field(FieldRef::TableColumn { table, column }, _) if table.is_empty() => {
729 column
730 }
731 Projection::Field(FieldRef::TableColumn { table, column }, _) => {
732 format!("{table}.{column}")
733 }
734 Projection::Function(name, args) => {
735 let rendered = args
736 .iter()
737 .map(render_group_by_function_arg)
738 .collect::<Option<Vec<_>>>()
739 .unwrap_or_default();
740 format!(
741 "{}({})",
742 name.split(':').next().unwrap_or(&name),
743 rendered.join(",")
744 )
745 }
746 Projection::Column(column) | Projection::Alias(column, _) => column,
747 Projection::All => "*".to_string(),
748 Projection::Expression(_, _) => "expr".to_string(),
749 Projection::Field(other, _) => format!("{other:?}"),
750 }),
751 }
752}
753
754fn render_group_by_function_arg(arg: &Projection) -> Option<String> {
755 match arg {
756 Projection::Column(col) => Some(
757 col.strip_prefix("LIT:")
758 .map(str::to_string)
759 .unwrap_or_else(|| col.clone()),
760 ),
761 Projection::All => Some("*".to_string()),
762 _ => None,
763 }
764}
765
766#[cfg(test)]
767mod tests {
768 use super::*;
769 use crate::storage::query::ast::{AsOfClause, BinOp, CompareOp, ExpandOptions, TableSource};
770
771 fn parse_table(sql: &str) -> TableQuery {
772 let parsed = super::super::parse(sql).unwrap().query;
773 let QueryExpr::Table(table) = parsed else {
774 panic!("expected table query");
775 };
776 table
777 }
778
779 fn col(name: &str) -> Expr {
780 Expr::Column {
781 field: FieldRef::TableColumn {
782 table: String::new(),
783 column: name.to_string(),
784 },
785 span: Span::synthetic(),
786 }
787 }
788
789 #[test]
790 fn helper_function_catalogs_cover_all_names() {
791 for name in [
792 "GEO_DISTANCE",
793 "GEO_DISTANCE_VINCENTY",
794 "GEO_BEARING",
795 "GEO_MIDPOINT",
796 "HAVERSINE",
797 "VINCENTY",
798 "TIME_BUCKET",
799 "UPPER",
800 "LOWER",
801 "LENGTH",
802 "CHAR_LENGTH",
803 "CHARACTER_LENGTH",
804 "OCTET_LENGTH",
805 "BIT_LENGTH",
806 "SUBSTRING",
807 "SUBSTR",
808 "POSITION",
809 "TRIM",
810 "LTRIM",
811 "RTRIM",
812 "BTRIM",
813 "CONCAT",
814 "CONCAT_WS",
815 "REVERSE",
816 "LEFT",
817 "RIGHT",
818 "QUOTE_LITERAL",
819 "ABS",
820 "ROUND",
821 "COALESCE",
822 "STDDEV",
823 "VARIANCE",
824 "MEDIAN",
825 "PERCENTILE",
826 "GROUP_CONCAT",
827 "STRING_AGG",
828 "FIRST",
829 "LAST",
830 "ARRAY_AGG",
831 "COUNT_DISTINCT",
832 "MONEY",
833 "MONEY_ASSET",
834 "MONEY_MINOR",
835 "MONEY_SCALE",
836 "VERIFY_PASSWORD",
837 "CAST",
838 "CASE",
839 ] {
840 assert!(is_scalar_function(name), "{name}");
841 }
842 assert!(!is_scalar_function("NOT_A_FUNCTION"));
843
844 for name in [
845 "COUNT",
846 "AVG",
847 "SUM",
848 "MIN",
849 "MAX",
850 "STDDEV",
851 "VARIANCE",
852 "MEDIAN",
853 "PERCENTILE",
854 "GROUP_CONCAT",
855 "STRING_AGG",
856 "FIRST",
857 "LAST",
858 "ARRAY_AGG",
859 "COUNT_DISTINCT",
860 ] {
861 assert!(is_aggregate_function(name), "{name}");
862 }
863 assert!(!is_aggregate_function("LOWER"));
864
865 assert_eq!(aggregate_token_name(&Token::Count), Some("COUNT"));
866 assert_eq!(aggregate_token_name(&Token::Sum), Some("SUM"));
867 assert_eq!(aggregate_token_name(&Token::Avg), Some("AVG"));
868 assert_eq!(aggregate_token_name(&Token::Min), Some("MIN"));
869 assert_eq!(aggregate_token_name(&Token::Max), Some("MAX"));
870 assert_eq!(aggregate_token_name(&Token::First), Some("FIRST"));
871 assert_eq!(aggregate_token_name(&Token::Last), Some("LAST"));
872 assert_eq!(aggregate_token_name(&Token::Ident("COUNT".into())), None);
873
874 assert_eq!(scalar_token_name(&Token::Left), Some("LEFT"));
875 assert_eq!(scalar_token_name(&Token::Right), Some("RIGHT"));
876 assert_eq!(scalar_token_name(&Token::Ident("LEFT".into())), None);
877
878 for unit in [
879 "ms",
880 "msec",
881 "millisecond",
882 "milliseconds",
883 "s",
884 "sec",
885 "secs",
886 "second",
887 "seconds",
888 "m",
889 "min",
890 "mins",
891 "minute",
892 "minutes",
893 "h",
894 "hr",
895 "hrs",
896 "hour",
897 "hours",
898 "d",
899 "day",
900 "days",
901 ] {
902 assert!(is_duration_unit(unit), "{unit}");
903 }
904 assert!(!is_duration_unit("fortnight"));
905 }
906
907 #[test]
908 fn projection_and_group_render_helpers_cover_aliases_and_exprs() {
909 let field = FieldRef::TableColumn {
910 table: String::new(),
911 column: "name".into(),
912 };
913 let filter = Filter::Compare {
914 field: field.clone(),
915 op: CompareOp::Eq,
916 value: Value::text("alice"),
917 };
918
919 assert_eq!(
920 attach_projection_alias(Projection::Field(field.clone(), None), Some("n".into())),
921 Projection::Field(field.clone(), Some("n".into()))
922 );
923 assert_eq!(
924 attach_projection_alias(
925 Projection::Expression(Box::new(filter.clone()), None),
926 Some("ok".into())
927 ),
928 Projection::Expression(Box::new(filter), Some("ok".into()))
929 );
930 assert_eq!(
931 attach_projection_alias(
932 Projection::Function("LOWER".into(), vec![]),
933 Some("l".into())
934 ),
935 Projection::Function("LOWER:l".into(), vec![])
936 );
937 assert_eq!(
938 attach_projection_alias(
939 Projection::Function("LOWER:l".into(), vec![]),
940 Some("ignored".into())
941 ),
942 Projection::Function("LOWER:l".into(), vec![])
943 );
944 assert_eq!(
945 attach_projection_alias(Projection::Column("name".into()), Some("n".into())),
946 Projection::Alias("name".into(), "n".into())
947 );
948 assert_eq!(
949 attach_projection_alias(Projection::All, Some("ignored".into())),
950 Projection::All
951 );
952
953 assert_eq!(render_group_by_expr(&col("dept")).as_deref(), Some("dept"));
954 assert_eq!(
955 render_group_by_expr(&Expr::Column {
956 field: FieldRef::TableColumn {
957 table: "employees".into(),
958 column: "dept".into()
959 },
960 span: Span::synthetic()
961 })
962 .as_deref(),
963 Some("employees.dept")
964 );
965 assert_eq!(
966 render_group_by_expr(&Expr::Column {
967 field: FieldRef::NodeId { alias: "n".into() },
968 span: Span::synthetic()
969 }),
970 Some("NodeId { alias: \"n\" }".into())
971 );
972 assert_eq!(
973 render_group_by_expr(&Expr::Literal {
974 value: Value::Null,
975 span: Span::synthetic()
976 })
977 .as_deref(),
978 Some("")
979 );
980 assert_eq!(
981 render_group_by_expr(&Expr::Literal {
982 value: Value::text("5m"),
983 span: Span::synthetic()
984 })
985 .as_deref(),
986 Some("5m")
987 );
988 assert_eq!(
989 render_group_by_expr(&Expr::Literal {
990 value: Value::Integer(7),
991 span: Span::synthetic()
992 })
993 .as_deref(),
994 Some("7")
995 );
996 assert_eq!(
997 render_group_by_expr(&Expr::FunctionCall {
998 name: "TIME_BUCKET".into(),
999 args: vec![
1000 col("ts"),
1001 Expr::Literal {
1002 value: Value::text("5m"),
1003 span: Span::synthetic()
1004 }
1005 ],
1006 span: Span::synthetic()
1007 })
1008 .as_deref(),
1009 Some("TIME_BUCKET(ts,5m)")
1010 );
1011 assert_eq!(
1012 render_group_by_expr(&Expr::FunctionCall {
1013 name: "LOWER".into(),
1014 args: vec![col("dept")],
1015 span: Span::synthetic()
1016 })
1017 .as_deref(),
1018 Some("LOWER()")
1019 );
1020
1021 assert_eq!(
1022 render_group_by_function_arg(&Projection::Column("LIT:5m".into())),
1023 Some("5m".into())
1024 );
1025 assert_eq!(
1026 render_group_by_function_arg(&Projection::Column("dept".into())),
1027 Some("dept".into())
1028 );
1029 assert_eq!(
1030 render_group_by_function_arg(&Projection::All),
1031 Some("*".into())
1032 );
1033 assert_eq!(
1034 render_group_by_function_arg(&Projection::Function("LOWER".into(), vec![])),
1035 None
1036 );
1037 }
1038
1039 #[test]
1040 fn expression_aggregate_detection_branches() {
1041 let count = Expr::FunctionCall {
1042 name: "COUNT".into(),
1043 args: vec![col("id")],
1044 span: Span::synthetic(),
1045 };
1046 assert!(contains_nested_aggregate(&count));
1047 assert!(is_plain_aggregate_expr(&count));
1048
1049 let nested = Expr::FunctionCall {
1050 name: "SUM".into(),
1051 args: vec![count.clone()],
1052 span: Span::synthetic(),
1053 };
1054 assert!(contains_nested_aggregate(&nested));
1055 assert!(!is_plain_aggregate_expr(&nested));
1056
1057 let binary = Expr::BinaryOp {
1058 op: BinOp::Add,
1059 lhs: Box::new(col("a")),
1060 rhs: Box::new(count.clone()),
1061 span: Span::synthetic(),
1062 };
1063 assert!(contains_nested_aggregate(&binary));
1064
1065 let unary = Expr::UnaryOp {
1066 op: UnaryOp::Not,
1067 operand: Box::new(count.clone()),
1068 span: Span::synthetic(),
1069 };
1070 assert!(contains_nested_aggregate(&unary));
1071
1072 let cast = Expr::Cast {
1073 inner: Box::new(count.clone()),
1074 target: crate::storage::schema::DataType::Integer,
1075 span: Span::synthetic(),
1076 };
1077 assert!(contains_nested_aggregate(&cast));
1078
1079 let case = Expr::Case {
1080 branches: vec![(col("flag"), count.clone())],
1081 else_: Some(Box::new(col("fallback"))),
1082 span: Span::synthetic(),
1083 };
1084 assert!(contains_nested_aggregate(&case));
1085
1086 let in_list = Expr::InList {
1087 target: Box::new(col("id")),
1088 values: vec![count.clone()],
1089 negated: false,
1090 span: Span::synthetic(),
1091 };
1092 assert!(contains_nested_aggregate(&in_list));
1093
1094 let between = Expr::Between {
1095 target: Box::new(col("id")),
1096 low: Box::new(col("low")),
1097 high: Box::new(count),
1098 negated: false,
1099 span: Span::synthetic(),
1100 };
1101 assert!(contains_nested_aggregate(&between));
1102 assert!(!contains_nested_aggregate(&Expr::Parameter {
1103 index: 1,
1104 span: Span::synthetic()
1105 }));
1106
1107 assert!(super::super::parse("SELECT SUM(COUNT(id)) FROM t").is_err());
1108 }
1109
1110 #[test]
1111 fn table_clause_parsing_covers_as_of_order_offset_and_expand() {
1112 let table = parse_table(
1113 "SELECT name FROM users AS OF COMMIT 'abc123' \
1114 WHERE deleted_at IS NULL \
1115 ORDER BY LOWER(name) ASC NULLS FIRST, created_at DESC NULLS LAST \
1116 LIMIT 10 OFFSET 5 WITH EXPAND GRAPH DEPTH 3, CROSS_REFS",
1117 );
1118 assert!(matches!(table.as_of, Some(AsOfClause::Commit(ref v)) if v == "abc123"));
1119 assert!(table.filter.is_some());
1120 assert_eq!(table.order_by.len(), 2);
1121 assert!(table.order_by[0].expr.is_some());
1122 assert!(table.order_by[0].ascending);
1123 assert!(table.order_by[0].nulls_first);
1124 assert!(!table.order_by[1].ascending);
1125 assert!(!table.order_by[1].nulls_first);
1126 assert_eq!(table.limit, Some(10));
1127 assert_eq!(table.offset, Some(5));
1128 assert!(matches!(
1129 table.expand,
1130 Some(ExpandOptions {
1131 graph: true,
1132 graph_depth: 3,
1133 cross_refs: true,
1134 ..
1135 })
1136 ));
1137
1138 let table = parse_table("SELECT * FROM users AS OF BRANCH 'main'");
1139 assert!(matches!(table.as_of, Some(AsOfClause::Branch(ref v)) if v == "main"));
1140
1141 let table = parse_table("SELECT * FROM users AS OF TAG 'v1'");
1142 assert!(matches!(table.as_of, Some(AsOfClause::Tag(ref v)) if v == "v1"));
1143
1144 let table = parse_table("SELECT * FROM users AS OF TIMESTAMP 1710000000000");
1145 assert!(matches!(
1146 table.as_of,
1147 Some(AsOfClause::TimestampMs(1_710_000_000_000))
1148 ));
1149
1150 let table = parse_table("SELECT * FROM users AS OF SNAPSHOT 42");
1151 assert!(matches!(table.as_of, Some(AsOfClause::Snapshot(42))));
1152
1153 let table = parse_table("SELECT * FROM users WITH EXPAND");
1154 assert!(matches!(
1155 table.expand,
1156 Some(ExpandOptions {
1157 graph: true,
1158 graph_depth: 1,
1159 cross_refs: true,
1160 ..
1161 })
1162 ));
1163
1164 assert!(super::super::parse("SELECT * FROM users AS OF SNAPSHOT -1").is_err());
1165 assert!(super::super::parse("SELECT * FROM users AS OF UNKNOWN 'x'").is_err());
1166 }
1167
1168 #[test]
1169 fn direct_parser_helpers_cover_projection_group_order_and_literals() {
1170 let mut parser = Parser::new("name, LOWER(email) AS email_l").unwrap();
1171 let projections = parser.parse_projection_list().unwrap();
1172 assert_eq!(projections.len(), 2);
1173
1174 let mut parser = Parser::new("dept, TIME_BUCKET(5 m)").unwrap();
1175 let group_by = parser.parse_group_by_list().unwrap();
1176 assert_eq!(group_by, vec!["dept", "TIME_BUCKET(5m)"]);
1177
1178 let mut parser = Parser::new("LOWER(name) DESC, created_at").unwrap();
1179 let order_by = parser.parse_order_by_list().unwrap();
1180 assert_eq!(order_by.len(), 2);
1181 assert!(order_by[0].expr.is_some());
1182 assert!(!order_by[0].ascending);
1183 assert!(order_by[0].nulls_first);
1184 assert!(order_by[1].ascending);
1185 assert!(!order_by[1].nulls_first);
1186
1187 let mut parser = Parser::new("-5 ms").unwrap();
1188 assert_eq!(parser.parse_function_literal_arg().unwrap(), "-5ms");
1189 let mut parser = Parser::new("2.0 H").unwrap();
1190 assert_eq!(parser.parse_function_literal_arg().unwrap(), "2h");
1191 let mut parser = Parser::new("bad").unwrap();
1192 assert!(parser.parse_function_literal_arg().is_err());
1193 }
1194
1195 #[test]
1196 fn from_subquery_source_is_preserved() {
1197 let parsed = super::super::parse("FROM (SELECT id FROM users) AS u RETURN u.id")
1198 .unwrap()
1199 .query;
1200 let QueryExpr::Table(table) = parsed else {
1201 panic!("expected table query");
1202 };
1203 assert_eq!(table.table, "__subq_u");
1204 assert_eq!(table.alias.as_deref(), Some("u"));
1205 assert!(matches!(table.source, Some(TableSource::Subquery(_))));
1206 assert_eq!(table.select_items.len(), 1);
1207
1208 assert!(super::super::parse("FROM (MATCH (n) RETURN n) AS g").is_err());
1209 }
1210}