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