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