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