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