Skip to main content

reddb_server/storage/query/parser/
table.rs

1//! Table query parsing (SELECT ... FROM ...)
2
3use 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    /// Parse SELECT ... FROM ... query
110    pub fn parse_select_query(&mut self) -> Result<QueryExpr, ParseError> {
111        // Recursion guard: nested subqueries (UNION, derived tables,
112        // EXISTS) re-enter through this point, so depth here bounds
113        // the SELECT-shaped recursion in addition to the expr Pratt
114        // climb guarded in `parse_expr_prec`.
115        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        // Parse column list
125        let (select_items, columns) = self.parse_select_items_and_projections()?;
126
127        // Parse optional table source. If omitted, default to `ANY` so the query
128        // can return mixed entities (table, document, graph, and vector) by default.
129        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        // Parse optional alias (only when a FROM clause exists).
161        // `AS OF` is a clause — don't gobble the `AS` as an alias
162        // marker when the following token is `OF`.
163        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        // Parse optional clauses
196        self.parse_table_clauses(&mut query)?;
197
198        Ok(QueryExpr::Table(query))
199    }
200}
201
202impl<'a> Parser<'a> {
203    /// Check if current identifier is a clause keyword
204    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    /// Parse projection list (column selections)
220    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        // Handle SELECT *
228        if self.consume(&Token::Star)? {
229            return Ok((vec![SelectItem::Wildcard], Vec::new())); // Empty legacy vec means all columns
230        }
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    /// Parse a single projection — supports columns, aggregate functions, and scalar functions
247    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    /// Parse table query clauses (AS OF, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, OFFSET)
360    pub fn parse_table_clauses(&mut self, query: &mut TableQuery) -> Result<(), ParseError> {
361        // AS OF clause — time-travel anchor. Must come before WHERE
362        // so the executor can bind the snapshot before filter eval.
363        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        // WHERE clause
373        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        // GROUP BY clause
380        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        // HAVING clause (only valid after GROUP BY)
388        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        // ORDER BY clause
395        if self.consume(&Token::Order)? {
396            self.expect(Token::By)?;
397            query.order_by = self.parse_order_by_list()?;
398        }
399
400        // LIMIT clause
401        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        // OFFSET clause
411        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        // WITH EXPAND clause
421        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    /// Parse an AS OF spec after `AS OF` has already been consumed.
429    /// Grammar:
430    ///   AS OF COMMIT   '<hex>'
431    ///   AS OF BRANCH   '<name>'
432    ///   AS OF TAG      '<name>'
433    ///   AS OF TIMESTAMP <integer-ms>
434    ///   AS OF SNAPSHOT  <xid>
435    fn parse_as_of_spec(&mut self) -> Result<crate::storage::query::ast::AsOfClause, ParseError> {
436        use crate::storage::query::ast::AsOfClause;
437
438        // Keyword — accept both tokenized forms (e.g. Token::Commit
439        // if present) and bare identifiers for flexibility.
440        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    /// Parse EXPAND options: GRAPH [DEPTH n], CROSS_REFS, ALL
495    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    /// Parse GROUP BY field list
536    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    /// Parse ORDER BY list.
562    ///
563    /// Fase 1.6 unlock: uses the new `Expr` Pratt parser so
564    /// `ORDER BY CAST(age AS INT)`, `ORDER BY a + b * 2`,
565    /// `ORDER BY last_seen - created_at` all parse cleanly. If the
566    /// parsed expression is a bare `Column`, we store it in the
567    /// legacy `field` slot and leave `expr` None so downstream
568    /// consumers (planner cost, mode translators) keep using the
569    /// fast path. Otherwise we stash the full tree in `expr` and
570    /// populate `field` with a synthetic marker that runtime code
571    /// never touches.
572    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                    // Synthetic placeholder so legacy pattern-matches
581                    // on `OrderByClause.field` still destructure.
582                    // Runtime comparators check `expr` first when set,
583                    // so the sentinel never gets resolved against a
584                    // real record.
585                    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 // Default: nulls last for ASC, first for DESC
609            };
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                    // F-05: `other` is a `Token` whose Display arms emit raw
646                    // user bytes for `Ident` / `String` / `JsonLiteral`.
647                    // Render via `{:?}` so CR/LF/NUL/quotes are escaped
648                    // before the message reaches downstream serialization
649                    // sinks.
650                    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}