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