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