Skip to main content

reddb_rql/parser/
table.rs

1//! Table query parsing (SELECT ... FROM ...)
2
3use super::error::ParseError;
4use crate::ast::{
5    BinOp, CompareOp, Expr, FieldRef, Filter, OrderByClause, Projection, QueryExpr,
6    QueueSelectQuery, SelectItem, Span, TableQuery, UnaryOp,
7};
8use crate::lexer::Token;
9use crate::sql_lowering::{expr_to_projection, filter_to_expr, select_item_to_projection};
10use reddb_types::types::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    /// Parse the comma-separated argument list of a table-valued function
122    /// call. The opening `(` has already been consumed; the caller consumes
123    /// the closing `)`. Requires at least one argument and rejects malformed
124    /// forms (issue #795).
125    ///
126    /// Three argument shapes are accepted (issues #796 / #799):
127    /// - positional identifiers, e.g. the graph collection `g`;
128    /// - named numeric arguments `key => <number>`, e.g. `resolution => 0.5`;
129    /// - named subquery arguments `key => (<SELECT …>)`, e.g.
130    ///   `nodes => (SELECT id FROM hosts)` (the inline-graph form).
131    ///
132    /// Positional arguments must precede named arguments; a positional
133    /// argument after any named one is a clear error. Returns the positional
134    /// identifiers, the named numeric `(key, value)` pairs, and the named
135    /// subquery `(key, query)` pairs, each in source order.
136    #[allow(clippy::type_complexity)]
137    fn parse_table_function_args(
138        &mut self,
139        name: &str,
140    ) -> Result<(Vec<String>, Vec<(String, f64)>, Vec<(String, QueryExpr)>), ParseError> {
141        // Zero-argument form `name()` is rejected with a clear message.
142        if matches!(self.peek(), Token::RParen) {
143            return Err(ParseError::new(
144                format!("table function '{name}' requires at least one argument"),
145                self.position(),
146            ));
147        }
148
149        let mut args = Vec::new();
150        let mut named_args = Vec::new();
151        let mut subquery_args: Vec<(String, QueryExpr)> = Vec::new();
152        loop {
153            // Each argument starts with an identifier (the positional value or
154            // the named-argument key). A handful of named-argument keys lex as
155            // reserved keywords (e.g. `max_iterations`); accept those here so
156            // the centrality TVFs (issue #797) can name them, mapping the token
157            // back to its lowercase identifier spelling.
158            let ident = match self.advance()? {
159                Token::Ident(arg) | Token::String(arg) => arg,
160                Token::MaxIterations => "max_iterations".to_string(),
161                other => {
162                    return Err(ParseError::expected(
163                        vec!["table function argument identifier or string literal"],
164                        &other,
165                        self.position(),
166                    ));
167                }
168            };
169
170            // `ident => (SELECT …)` is a named subquery argument;
171            // `ident => <number>` is a named numeric argument; otherwise it is
172            // a bare positional identifier.
173            if matches!(self.peek(), Token::FatArrow) {
174                self.advance()?; // consume '=>'
175                if matches!(self.peek(), Token::LParen) {
176                    self.advance()?; // consume '('
177                    if !self.check(&Token::Select) {
178                        let found = self.peek().clone();
179                        return Err(ParseError::expected(
180                            vec!["SELECT subquery"],
181                            &found,
182                            self.position(),
183                        ));
184                    }
185                    let query = self.parse_select_query()?;
186                    self.expect(Token::RParen)?;
187                    subquery_args.push((ident, query));
188                } else {
189                    let value = self.parse_float()?;
190                    named_args.push((ident, value));
191                }
192            } else if named_args.is_empty() && subquery_args.is_empty() {
193                args.push(ident);
194            } else {
195                return Err(ParseError::new(
196                    format!(
197                        "table function '{name}' positional argument '{ident}' cannot follow a named argument"
198                    ),
199                    self.position(),
200                ));
201            }
202
203            // A comma continues the list; a `)` ends it (consumed by caller).
204            // Anything else is a clear error (e.g. a missing closing paren).
205            match self.peek() {
206                Token::Comma => {
207                    self.advance()?;
208                    continue;
209                }
210                Token::RParen => break,
211                _ => {
212                    let found = self.peek().clone();
213                    return Err(ParseError::expected(
214                        vec!["','", "')'"],
215                        &found,
216                        self.position(),
217                    ));
218                }
219            }
220        }
221        Ok((args, named_args, subquery_args))
222    }
223
224    /// Build the `TableSource` for a table-valued function call from its
225    /// parsed argument lists. When `subquery_args` is non-empty the call uses
226    /// the inline-graph form (`nodes => / edges =>`); otherwise it is the
227    /// graph-collection form (issue #799).
228    fn build_table_function_source(
229        &self,
230        name: String,
231        args: Vec<String>,
232        named_args: Vec<(String, f64)>,
233        subquery_args: Vec<(String, QueryExpr)>,
234    ) -> Result<crate::ast::TableSource, ParseError> {
235        use crate::ast::TableSource;
236
237        if subquery_args.is_empty() {
238            return Ok(TableSource::Function {
239                name,
240                args,
241                named_args,
242            });
243        }
244
245        // Inline-graph form: exactly one `nodes` and one `edges` subquery, no
246        // positional graph-collection argument.
247        if !args.is_empty() {
248            return Err(ParseError::new(
249                format!(
250                    "table function '{name}' inline form takes no positional graph argument; pass `nodes => (…), edges => (…)`"
251                ),
252                self.position(),
253            ));
254        }
255
256        let mut nodes: Option<QueryExpr> = None;
257        let mut edges: Option<QueryExpr> = None;
258        for (key, query) in subquery_args {
259            if key.eq_ignore_ascii_case("nodes") {
260                if nodes.is_some() {
261                    return Err(ParseError::new(
262                        format!(
263                            "table function '{name}' has a duplicate 'nodes' subquery argument"
264                        ),
265                        self.position(),
266                    ));
267                }
268                nodes = Some(query);
269            } else if key.eq_ignore_ascii_case("edges") {
270                if edges.is_some() {
271                    return Err(ParseError::new(
272                        format!(
273                            "table function '{name}' has a duplicate 'edges' subquery argument"
274                        ),
275                        self.position(),
276                    ));
277                }
278                edges = Some(query);
279            } else {
280                return Err(ParseError::new(
281                    format!(
282                        "table function '{name}' has no subquery argument '{key}' (expected 'nodes' or 'edges')"
283                    ),
284                    self.position(),
285                ));
286            }
287        }
288
289        let (Some(nodes), Some(edges)) = (nodes, edges) else {
290            return Err(ParseError::new(
291                format!(
292                    "table function '{name}' inline form requires both `nodes => (…)` and `edges => (…)` subqueries"
293                ),
294                self.position(),
295            ));
296        };
297
298        Ok(TableSource::InlineGraphFunction {
299            name,
300            nodes: Box::new(nodes),
301            edges: Box::new(edges),
302            named_args,
303        })
304    }
305
306    /// Read one segment of a dotted table name. The graph analytics outputs
307    /// `components` and `centrality` lex as reserved keywords, so they are
308    /// normalised back to their lowercase spelling here; every other segment
309    /// (e.g. `communities`, or virtual-schema suffixes like `collections`) is
310    /// an ordinary identifier. Issue #800.
311    fn parse_table_name_segment(&mut self) -> Result<String, ParseError> {
312        match self.peek() {
313            Token::Components => {
314                self.advance()?;
315                Ok("components".to_string())
316            }
317            Token::Centrality => {
318                self.advance()?;
319                Ok("centrality".to_string())
320            }
321            _ => self.expect_ident(),
322        }
323    }
324
325    fn parse_select_query_inner(&mut self) -> Result<QueryExpr, ParseError> {
326        self.expect(Token::Select)?;
327
328        // `SELECT DISTINCT <projection>` — the projection-level quantifier
329        // (issue #1126). Detected immediately after SELECT, before the
330        // projection list, so it never collides with the aggregate-argument
331        // form `COUNT(DISTINCT x)`, which is parsed inside the call args.
332        let distinct = self.consume(&Token::Distinct)?;
333
334        // Parse column list
335        let (select_items, columns) = self.parse_select_items_and_projections()?;
336
337        // Parse optional table source. If omitted, default to `ANY` so the query
338        // can return mixed entities (table, document, graph, and vector) by default.
339        let has_from = self.consume(&Token::From)?;
340        // Optional structured FROM source. Currently populated only for
341        // table-valued function calls such as `components(g)` (issue #795);
342        // plain tables leave this `None` and rely on the legacy `table` slot.
343        let mut table_source: Option<crate::ast::TableSource> = None;
344        let mut from_subquery: Option<QueryExpr> = None;
345        let table = if has_from {
346            if self.consume(&Token::Queue)? {
347                let queue = self.expect_ident()?;
348                let filter = if self.consume(&Token::Where)? {
349                    Some(self.parse_filter()?)
350                } else {
351                    None
352                };
353                let limit = if self.consume(&Token::Limit)? {
354                    Some(self.parse_integer()? as u64)
355                } else {
356                    None
357                };
358                return Ok(QueryExpr::QueueSelect(QueueSelectQuery {
359                    queue,
360                    columns: queue_projection_columns(&columns)?,
361                    filter,
362                    limit,
363                }));
364            } else if self.check(&Token::LParen) {
365                self.advance()?; // consume '('
366                if !self.check(&Token::Select) {
367                    return Err(ParseError::new(
368                        "subquery in FROM must start with SELECT".to_string(),
369                        self.position(),
370                    ));
371                }
372                from_subquery = Some(self.parse_select_query()?);
373                self.expect(Token::RParen)?;
374                "__subq_pending".to_string()
375            } else if self.consume(&Token::Star)? {
376                "*".to_string()
377            } else if self.consume(&Token::All)? {
378                "all".to_string()
379            } else if matches!(self.peek(), Token::Components) {
380                // `components` lexes as the graph-analytics keyword
381                // `Token::Components`, so it never reaches `expect_ident`.
382                // In FROM position it is the connected-components
383                // table-valued function: `FROM components(g)` (issue #795).
384                self.advance()?; // consume COMPONENTS
385                let name = "components".to_string();
386                self.expect(Token::LParen)?;
387                let (args, named_args, subquery_args) = self.parse_table_function_args(&name)?;
388                self.expect(Token::RParen)?;
389                table_source = Some(self.build_table_function_source(
390                    name.clone(),
391                    args,
392                    named_args,
393                    subquery_args,
394                )?);
395                name
396            } else if matches!(self.peek(), Token::ShortestPath) {
397                // `shortest_path` lexes as the graph-analytics keyword
398                // `Token::ShortestPath`, so it never reaches `expect_ident`.
399                // In FROM position it is the shortest-path table-valued
400                // function: `FROM shortest_path(g, src => .., dst => ..)`
401                // (issue #798). The bare `SHORTEST_PATH ... FROM ... TO ...`
402                // graph-command form is dispatched separately at statement
403                // start, so the two grammars never collide.
404                self.advance()?; // consume SHORTEST_PATH
405                let name = "shortest_path".to_string();
406                self.expect(Token::LParen)?;
407                let (args, named_args, subquery_args) = self.parse_table_function_args(&name)?;
408                self.expect(Token::RParen)?;
409                table_source = Some(self.build_table_function_source(
410                    name.clone(),
411                    args,
412                    named_args,
413                    subquery_args,
414                )?);
415                name
416            } else {
417                let ident = self.expect_ident()?;
418                let mut name = ident;
419                while matches!(self.peek(), Token::Dot) {
420                    self.advance()?; // consume '.'
421                    let segment = self.parse_table_name_segment()?;
422                    name.push('.');
423                    name.push_str(&segment);
424                }
425                // Table-valued function call: `ident(arg, ...)` (issue #795).
426                if matches!(self.peek(), Token::LParen) {
427                    self.advance()?; // consume '('
428                    let (args, named_args, subquery_args) =
429                        self.parse_table_function_args(&name)?;
430                    self.expect(Token::RParen)?;
431                    table_source = Some(self.build_table_function_source(
432                        name.clone(),
433                        args,
434                        named_args,
435                        subquery_args,
436                    )?);
437                    name
438                } else {
439                    name
440                }
441            }
442        } else {
443            "any".to_string()
444        };
445
446        // Parse optional alias (only when a FROM clause exists).
447        // `AS OF` is a clause — don't gobble the `AS` as an alias
448        // marker when the following token is `OF`.
449        let alias =
450            if !has_from || (self.check(&Token::As) && matches!(self.peek_next()?, Token::Of)) {
451                None
452            } else if self.consume(&Token::As)?
453                || (self.check(&Token::Ident("".into())) && !self.is_clause_keyword())
454            {
455                Some(self.expect_ident()?)
456            } else {
457                None
458            };
459
460        let mut query = if let Some(inner) = from_subquery {
461            let mut query = TableQuery::from_subquery(inner, alias);
462            query.select_items = select_items;
463            query.columns = columns;
464            query.distinct = distinct;
465            query
466        } else {
467            TableQuery {
468                table,
469                source: table_source,
470                alias,
471                select_items,
472                columns,
473                where_expr: None,
474                filter: None,
475                group_by_exprs: Vec::new(),
476                group_by: Vec::new(),
477                having_expr: None,
478                having: None,
479                order_by: Vec::new(),
480                limit: None,
481                limit_param: None,
482                offset: None,
483                offset_param: None,
484                expand: None,
485                as_of: None,
486                sessionize: None,
487                distinct,
488            }
489        };
490
491        if self.is_join_keyword() {
492            let return_items = std::mem::take(&mut query.select_items);
493            let return_ = std::mem::take(&mut query.columns);
494            let mut expr = self.parse_join_query(QueryExpr::Table(query))?;
495            if let QueryExpr::Join(join) = &mut expr {
496                join.return_items = return_items;
497                join.return_ = return_;
498            }
499            return Ok(expr);
500        }
501
502        // SESSIONIZE BY <ident> GAP <duration> [ORDER BY <ident>]
503        // — issue #585 slice 8. Parsed before WHERE/GROUP BY so the
504        // optional inner ORDER BY (which the user binds to the
505        // operator's timestamp axis) cannot be confused with the
506        // SELECT's top-level ORDER BY further down. Both `BY` and
507        // `GAP` may be omitted when the source collection's
508        // descriptor carries `SESSION_KEY` / `SESSION_GAP` defaults
509        // (slice 1) — the executor resolves them at run time and
510        // raises `MissingSessionKey` if neither side supplies a
511        // value.
512        if self.consume(&Token::Sessionize)? {
513            query.sessionize = Some(self.parse_sessionize_clause()?);
514        }
515
516        // Parse optional clauses
517        self.parse_table_clauses(&mut query)?;
518
519        Ok(QueryExpr::Table(query))
520    }
521
522    fn parse_sessionize_clause(&mut self) -> Result<crate::ast::SessionizeClause, ParseError> {
523        use crate::ast::SessionizeClause;
524
525        let mut clause = SessionizeClause::default();
526
527        if self.consume(&Token::By)? {
528            clause.actor_col = Some(self.expect_ident()?);
529        }
530        if self.consume(&Token::Gap)? {
531            let value = self.parse_float()?;
532            let unit = self.parse_duration_unit()?;
533            clause.gap_ms = Some((value * unit) as u64);
534        }
535        // Optional `ORDER BY <ident>` immediately after GAP. The
536        // top-level SELECT ORDER BY parsed by `parse_table_clauses`
537        // sees the next ORDER token, so this only consumes the one
538        // immediately attached to SESSIONIZE.
539        if self.consume(&Token::Order)? {
540            self.expect(Token::By)?;
541            clause.order_col = Some(self.expect_ident()?);
542        }
543        Ok(clause)
544    }
545}
546
547impl<'a> Parser<'a> {
548    /// Check if current identifier is a clause keyword
549    pub fn is_clause_keyword(&self) -> bool {
550        matches!(
551            self.peek(),
552            Token::Where
553                | Token::Order
554                | Token::Limit
555                | Token::Offset
556                | Token::Join
557                | Token::Inner
558                | Token::Left
559                | Token::Right
560                | Token::As
561                | Token::Sessionize
562        )
563    }
564
565    /// Parse projection list (column selections)
566    pub fn parse_projection_list(&mut self) -> Result<Vec<Projection>, ParseError> {
567        Ok(self.parse_select_items_and_projections()?.1)
568    }
569
570    pub(crate) fn parse_select_items_and_projections(
571        &mut self,
572    ) -> Result<(Vec<SelectItem>, Vec<Projection>), ParseError> {
573        // Handle SELECT *
574        if self.consume(&Token::Star)? {
575            return Ok((vec![SelectItem::Wildcard], Vec::new())); // Empty legacy vec means all columns
576        }
577
578        let mut select_items = Vec::new();
579        let mut projections = Vec::new();
580        loop {
581            let (item, proj) = self.parse_projection()?;
582            select_items.push(item);
583            projections.push(proj);
584
585            if !self.consume(&Token::Comma)? {
586                break;
587            }
588        }
589        Ok((select_items, projections))
590    }
591
592    /// Parse a single projection — supports columns, aggregate functions, and scalar functions
593    fn parse_projection(&mut self) -> Result<(SelectItem, Projection), ParseError> {
594        let expr = self.parse_expr()?;
595        if contains_nested_aggregate(&expr) && !is_plain_aggregate_expr(&expr) {
596            return Err(ParseError::new(
597                "aggregate function is not valid inside another expression".to_string(),
598                self.position(),
599            ));
600        }
601        let alias = if self.consume(&Token::As)? {
602            Some(self.expect_column_ident()?)
603        } else {
604            None
605        };
606        let select_item = SelectItem::Expr {
607            expr: expr.clone(),
608            alias: alias.clone(),
609        };
610        let projection = select_item_to_projection(&select_item).ok_or_else(|| {
611            ParseError::new(
612                "projection cannot yet be lowered to legacy runtime representation".to_string(),
613                self.position(),
614            )
615        })?;
616        Ok((select_item, projection))
617    }
618}
619
620fn contains_nested_aggregate(expr: &Expr) -> bool {
621    match expr {
622        Expr::FunctionCall { name, args, .. } => {
623            is_aggregate_function(&name.to_uppercase())
624                || args.iter().any(contains_nested_aggregate)
625        }
626        // Issue #589 slice 7a: a window function aggregate (e.g.
627        // `SUM(x) OVER (...)`) is NOT a plain aggregate from the
628        // group-by analyser's point of view — it operates over a
629        // partitioned window, not a GROUP BY group. We still recurse
630        // into args / partition / order keys so a *nested* aggregate
631        // (e.g. `SUM(COUNT(*) OVER ()) OVER (...)`) is caught.
632        Expr::WindowFunctionCall { args, window, .. } => {
633            args.iter().any(contains_nested_aggregate)
634                || window.partition_by.iter().any(contains_nested_aggregate)
635                || window
636                    .order_by
637                    .iter()
638                    .any(|o| contains_nested_aggregate(&o.expr))
639        }
640        Expr::BinaryOp { lhs, rhs, .. } => {
641            contains_nested_aggregate(lhs) || contains_nested_aggregate(rhs)
642        }
643        Expr::UnaryOp { operand, .. } | Expr::IsNull { operand, .. } => {
644            contains_nested_aggregate(operand)
645        }
646        Expr::Cast { inner, .. } => contains_nested_aggregate(inner),
647        Expr::Case {
648            branches, else_, ..
649        } => {
650            branches.iter().any(|(cond, value)| {
651                contains_nested_aggregate(cond) || contains_nested_aggregate(value)
652            }) || else_.as_deref().is_some_and(contains_nested_aggregate)
653        }
654        Expr::InList { target, values, .. } => {
655            contains_nested_aggregate(target) || values.iter().any(contains_nested_aggregate)
656        }
657        Expr::Between {
658            target, low, high, ..
659        } => {
660            contains_nested_aggregate(target)
661                || contains_nested_aggregate(low)
662                || contains_nested_aggregate(high)
663        }
664        Expr::Literal { .. }
665        | Expr::Column { .. }
666        | Expr::Parameter { .. }
667        | Expr::Subquery { .. } => false,
668    }
669}
670
671fn is_plain_aggregate_expr(expr: &Expr) -> bool {
672    match expr {
673        Expr::FunctionCall { name, args, .. } if is_aggregate_function(&name.to_uppercase()) => {
674            !args.iter().any(contains_nested_aggregate)
675        }
676        _ => false,
677    }
678}
679
680fn attach_projection_alias(proj: Projection, alias: Option<String>) -> Projection {
681    let Some(alias) = alias else { return proj };
682    match proj {
683        Projection::Field(field, _) => Projection::Field(field, Some(alias)),
684        Projection::Expression(filter, _) => Projection::Expression(filter, Some(alias)),
685        Projection::Function(name, args) => {
686            if name.contains(':') {
687                Projection::Function(name, args)
688            } else {
689                Projection::Function(format!("{name}:{alias}"), args)
690            }
691        }
692        Projection::Column(column) => Projection::Alias(column, alias),
693        Projection::Window {
694            name, args, window, ..
695        } => Projection::Window {
696            name,
697            args,
698            window,
699            alias: Some(alias),
700        },
701        other => other,
702    }
703}
704
705fn queue_projection_columns(columns: &[Projection]) -> Result<Vec<String>, ParseError> {
706    let mut out = Vec::new();
707    for column in columns {
708        match column {
709            Projection::Column(name) => out.push(name.clone()),
710            Projection::Alias(name, _) => out.push(name.clone()),
711            Projection::Field(FieldRef::TableColumn { table, column }, _) if table.is_empty() => {
712                out.push(column.clone());
713            }
714            Projection::All => return Ok(Vec::new()),
715            other => {
716                return Err(ParseError::new(
717                    format!(
718                        "unsupported SELECT FROM QUEUE projection {other:?}; use `SELECT *` or bare column names, or use queue verbs (PUSH, POP, PEEK, LEN, ACK, NACK, …) for queue operations"
719                    ),
720                    crate::lexer::Position::default(),
721                ));
722            }
723        }
724    }
725    Ok(out)
726}
727
728impl<'a> Parser<'a> {
729    /// Parse table query clauses (AS OF, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, OFFSET)
730    pub fn parse_table_clauses(&mut self, query: &mut TableQuery) -> Result<(), ParseError> {
731        // AS OF clause — time-travel anchor. Must come before WHERE
732        // so the executor can bind the snapshot before filter eval.
733        if self.check(&Token::As) {
734            let next_is_of = matches!(self.peek_next()?, Token::Of);
735            if next_is_of {
736                self.expect(Token::As)?;
737                self.expect(Token::Of)?;
738                query.as_of = Some(self.parse_as_of_spec()?);
739            }
740        }
741
742        // WHERE clause
743        if self.consume(&Token::Where)? {
744            let filter = self.parse_filter()?;
745            query.where_expr = Some(filter_to_expr(&filter));
746            query.filter = Some(filter);
747        }
748
749        // GROUP BY clause
750        if self.consume(&Token::Group)? {
751            self.expect(Token::By)?;
752            let (group_by_exprs, group_by) = self.parse_group_by_items()?;
753            query.group_by_exprs = group_by_exprs;
754            query.group_by = group_by;
755        }
756
757        // HAVING clause (only valid after GROUP BY)
758        if !query.group_by_exprs.is_empty() && self.consume_ident_ci("HAVING")? {
759            let having = self.parse_filter()?;
760            query.having_expr = Some(filter_to_expr(&having));
761            query.having = Some(having);
762        }
763
764        // ORDER BY clause
765        if self.consume(&Token::Order)? {
766            self.expect(Token::By)?;
767            query.order_by = self.parse_order_by_list()?;
768        }
769
770        // LIMIT clause
771        if self.consume(&Token::Limit)? {
772            if matches!(self.peek(), Token::Dollar | Token::Question) {
773                query.limit_param = Some(self.parse_param_slot("LIMIT")?);
774                query.limit = None;
775            } else {
776                query.limit = Some(self.parse_integer()? as u64);
777            }
778        }
779
780        // OFFSET clause
781        if self.consume(&Token::Offset)? {
782            if matches!(self.peek(), Token::Dollar | Token::Question) {
783                query.offset_param = Some(self.parse_param_slot("OFFSET")?);
784                query.offset = None;
785            } else {
786                query.offset = Some(self.parse_integer()? as u64);
787            }
788        }
789
790        // WITH EXPAND clause
791        if self.consume(&Token::With)? && self.consume_ident_ci("EXPAND")? {
792            query.expand = Some(self.parse_expand_options()?);
793        }
794
795        Ok(())
796    }
797
798    /// Parse an AS OF spec after `AS OF` has already been consumed.
799    /// Grammar:
800    ///   AS OF COMMIT   '<hex>'
801    ///   AS OF BRANCH   '<name>'
802    ///   AS OF TAG      '<name>'
803    ///   AS OF TIMESTAMP <integer-ms>
804    ///   AS OF SNAPSHOT  <xid>
805    fn parse_as_of_spec(&mut self) -> Result<crate::ast::AsOfClause, ParseError> {
806        use crate::ast::AsOfClause;
807
808        // Keyword — accept both tokenized forms (e.g. Token::Commit
809        // if present) and bare identifiers for flexibility.
810        let keyword = match self.peek() {
811            Token::Ident(s) => {
812                let s = s.to_ascii_uppercase();
813                self.advance()?;
814                s
815            }
816            Token::Commit => {
817                self.advance()?;
818                "COMMIT".to_string()
819            }
820            other => {
821                return Err(ParseError::expected(
822                    vec!["COMMIT", "BRANCH", "TAG", "TIMESTAMP", "SNAPSHOT"],
823                    other,
824                    self.position(),
825                ));
826            }
827        };
828
829        match keyword.as_str() {
830            "COMMIT" => {
831                let value = self.parse_string()?;
832                Ok(AsOfClause::Commit(value))
833            }
834            "BRANCH" => {
835                let value = self.parse_string()?;
836                Ok(AsOfClause::Branch(value))
837            }
838            "TAG" => {
839                let value = self.parse_string()?;
840                Ok(AsOfClause::Tag(value))
841            }
842            "TIMESTAMP" => {
843                let value = self.parse_integer()?;
844                Ok(AsOfClause::TimestampMs(value))
845            }
846            "SNAPSHOT" => {
847                let value = self.parse_integer()?;
848                if value < 0 {
849                    return Err(ParseError::new(
850                        "AS OF SNAPSHOT requires non-negative xid".to_string(),
851                        self.position(),
852                    ));
853                }
854                Ok(AsOfClause::Snapshot(value as u64))
855            }
856            other => Err(ParseError::expected(
857                vec!["COMMIT", "BRANCH", "TAG", "TIMESTAMP", "SNAPSHOT"],
858                &Token::Ident(other.into()),
859                self.position(),
860            )),
861        }
862    }
863
864    /// Parse EXPAND options: GRAPH [DEPTH n], CROSS_REFS, ALL
865    fn parse_expand_options(&mut self) -> Result<crate::ast::ExpandOptions, ParseError> {
866        use crate::ast::ExpandOptions;
867        let mut opts = ExpandOptions::default();
868
869        loop {
870            if self.consume(&Token::Graph)? || self.consume_ident_ci("GRAPH")? {
871                opts.graph = true;
872                opts.graph_depth = if self.consume(&Token::Depth)? {
873                    self.parse_integer()? as usize
874                } else {
875                    1
876                };
877            } else if self.consume_ident_ci("CROSS_REFS")?
878                || self.consume_ident_ci("CROSSREFS")?
879                || self.consume_ident_ci("REFS")?
880            {
881                opts.cross_refs = true;
882            } else if self.consume(&Token::All)? || self.consume_ident_ci("ALL")? {
883                opts.graph = true;
884                opts.cross_refs = true;
885                opts.graph_depth = 1;
886            } else {
887                break;
888            }
889            if !self.consume(&Token::Comma)? {
890                break;
891            }
892        }
893
894        if !opts.graph && !opts.cross_refs {
895            opts.graph = true;
896            opts.cross_refs = true;
897            opts.graph_depth = 1;
898        }
899
900        Ok(opts)
901    }
902
903    /// Parse GROUP BY field list
904    pub fn parse_group_by_list(&mut self) -> Result<Vec<String>, ParseError> {
905        Ok(self.parse_group_by_items()?.1)
906    }
907
908    fn parse_group_by_items(&mut self) -> Result<(Vec<Expr>, Vec<String>), ParseError> {
909        let mut exprs = Vec::new();
910        let mut fields = Vec::new();
911        loop {
912            let expr = self.parse_expr()?;
913            let rendered = render_group_by_expr(&expr).ok_or_else(|| {
914                ParseError::new(
915                    "GROUP BY expression cannot yet be lowered to legacy runtime representation"
916                        .to_string(),
917                    self.position(),
918                )
919            })?;
920            exprs.push(expr);
921            fields.push(rendered);
922            if !self.consume(&Token::Comma)? {
923                break;
924            }
925        }
926        Ok((exprs, fields))
927    }
928
929    /// Parse ORDER BY list.
930    ///
931    /// Fase 1.6 unlock: uses the new `Expr` Pratt parser so
932    /// `ORDER BY CAST(age AS INT)`, `ORDER BY a + b * 2`,
933    /// `ORDER BY last_seen - created_at` all parse cleanly. If the
934    /// parsed expression is a bare `Column`, we store it in the
935    /// legacy `field` slot and leave `expr` None so downstream
936    /// consumers (planner cost, mode translators) keep using the
937    /// fast path. Otherwise we stash the full tree in `expr` and
938    /// populate `field` with a synthetic marker that runtime code
939    /// never touches.
940    pub fn parse_order_by_list(&mut self) -> Result<Vec<OrderByClause>, ParseError> {
941        use crate::ast::Expr as AstExpr;
942        let mut clauses = Vec::new();
943        loop {
944            let parsed = self.parse_expr()?;
945            let (field, expr_slot) = match parsed {
946                AstExpr::Column { field, .. } => (field, None),
947                other => (
948                    // Synthetic placeholder so legacy pattern-matches
949                    // on `OrderByClause.field` still destructure.
950                    // Runtime comparators check `expr` first when set,
951                    // so the sentinel never gets resolved against a
952                    // real record.
953                    FieldRef::TableColumn {
954                        table: String::new(),
955                        column: String::new(),
956                    },
957                    Some(other),
958                ),
959            };
960
961            let ascending = if self.consume(&Token::Desc)? {
962                false
963            } else {
964                self.consume(&Token::Asc)?;
965                true
966            };
967
968            let nulls_first = if self.consume(&Token::Nulls)? {
969                if self.consume(&Token::First)? {
970                    true
971                } else {
972                    self.expect(Token::Last)?;
973                    false
974                }
975            } else {
976                !ascending // Default: nulls last for ASC, first for DESC
977            };
978
979            clauses.push(OrderByClause {
980                field,
981                expr: expr_slot,
982                ascending,
983                nulls_first,
984            });
985
986            if !self.consume(&Token::Comma)? {
987                break;
988            }
989        }
990        Ok(clauses)
991    }
992
993    fn parse_function_literal_arg(&mut self) -> Result<String, ParseError> {
994        let negative = self.consume(&Token::Dash)?;
995        let mut literal = match self.advance()? {
996            Token::Integer(n) => {
997                if negative {
998                    format!("-{n}")
999                } else {
1000                    n.to_string()
1001                }
1002            }
1003            Token::Float(n) => {
1004                let value = if negative { -n } else { n };
1005                if value.fract().abs() < f64::EPSILON {
1006                    format!("{}", value as i64)
1007                } else {
1008                    value.to_string()
1009                }
1010            }
1011            other => {
1012                return Err(ParseError::new(
1013                    // F-05: `other` is a `Token` whose Display arms emit raw
1014                    // user bytes for `Ident` / `String` / `JsonLiteral`.
1015                    // Render via `{:?}` so CR/LF/NUL/quotes are escaped
1016                    // before the message reaches downstream serialization
1017                    // sinks.
1018                    format!("expected number, got {:?}", other),
1019                    self.position(),
1020                ));
1021            }
1022        };
1023
1024        if let Token::Ident(unit) = self.peek().clone() {
1025            if is_duration_unit(&unit) {
1026                self.advance()?;
1027                literal.push_str(&unit.to_ascii_lowercase());
1028            }
1029        }
1030
1031        Ok(literal)
1032    }
1033}
1034
1035fn is_duration_unit(unit: &str) -> bool {
1036    matches!(
1037        unit.to_ascii_lowercase().as_str(),
1038        "ms" | "msec"
1039            | "millisecond"
1040            | "milliseconds"
1041            | "s"
1042            | "sec"
1043            | "secs"
1044            | "second"
1045            | "seconds"
1046            | "m"
1047            | "min"
1048            | "mins"
1049            | "minute"
1050            | "minutes"
1051            | "h"
1052            | "hr"
1053            | "hrs"
1054            | "hour"
1055            | "hours"
1056            | "d"
1057            | "day"
1058            | "days"
1059    )
1060}
1061
1062fn render_group_by_expr(expr: &Expr) -> Option<String> {
1063    match expr {
1064        Expr::Column { field, .. } => match field {
1065            FieldRef::TableColumn { table, column } if table.is_empty() => Some(column.clone()),
1066            FieldRef::TableColumn { table, column } => Some(format!("{table}.{column}")),
1067            other => Some(format!("{other:?}")),
1068        },
1069        Expr::FunctionCall { name, args, .. } if name.eq_ignore_ascii_case("TIME_BUCKET") => {
1070            let rendered = args
1071                .iter()
1072                .map(render_group_by_expr)
1073                .collect::<Option<Vec<_>>>()?;
1074            Some(format!("TIME_BUCKET({})", rendered.join(",")))
1075        }
1076        Expr::Literal { value, .. } => Some(match value {
1077            Value::Null => String::new(),
1078            Value::Text(text) => text.to_string(),
1079            other => other.to_string(),
1080        }),
1081        _ => expr_to_projection(expr).map(|projection| match projection {
1082            Projection::Field(FieldRef::TableColumn { table, column }, _) if table.is_empty() => {
1083                column
1084            }
1085            Projection::Field(FieldRef::TableColumn { table, column }, _) => {
1086                format!("{table}.{column}")
1087            }
1088            Projection::Function(name, args) => {
1089                let rendered = args
1090                    .iter()
1091                    .map(render_group_by_function_arg)
1092                    .collect::<Option<Vec<_>>>()
1093                    .unwrap_or_default();
1094                format!(
1095                    "{}({})",
1096                    name.split(':').next().unwrap_or(&name),
1097                    rendered.join(",")
1098                )
1099            }
1100            Projection::Column(column) | Projection::Alias(column, _) => column,
1101            Projection::All => "*".to_string(),
1102            Projection::Expression(_, _) => "expr".to_string(),
1103            Projection::Field(other, _) => format!("{other:?}"),
1104            Projection::Window { name, .. } => name,
1105        }),
1106    }
1107}
1108
1109fn render_group_by_function_arg(arg: &Projection) -> Option<String> {
1110    match arg {
1111        Projection::Column(col) => Some(
1112            col.strip_prefix("LIT:")
1113                .map(str::to_string)
1114                .unwrap_or_else(|| col.clone()),
1115        ),
1116        Projection::All => Some("*".to_string()),
1117        _ => None,
1118    }
1119}
1120
1121#[cfg(test)]
1122mod tests {
1123    use super::*;
1124    use crate::ast::{AsOfClause, BinOp, CompareOp, ExpandOptions, TableSource};
1125
1126    fn parse_table(sql: &str) -> TableQuery {
1127        let parsed = crate::parser::parse(sql).unwrap().query;
1128        let QueryExpr::Table(table) = parsed else {
1129            panic!("expected table query");
1130        };
1131        table
1132    }
1133
1134    fn col(name: &str) -> Expr {
1135        Expr::Column {
1136            field: FieldRef::TableColumn {
1137                table: String::new(),
1138                column: name.to_string(),
1139            },
1140            span: Span::synthetic(),
1141        }
1142    }
1143
1144    #[test]
1145    fn helper_function_catalogs_cover_all_names() {
1146        for name in [
1147            "GEO_DISTANCE",
1148            "GEO_DISTANCE_VINCENTY",
1149            "GEO_BEARING",
1150            "GEO_MIDPOINT",
1151            "HAVERSINE",
1152            "VINCENTY",
1153            "TIME_BUCKET",
1154            "UPPER",
1155            "LOWER",
1156            "LENGTH",
1157            "CHAR_LENGTH",
1158            "CHARACTER_LENGTH",
1159            "OCTET_LENGTH",
1160            "BIT_LENGTH",
1161            "SUBSTRING",
1162            "SUBSTR",
1163            "POSITION",
1164            "TRIM",
1165            "LTRIM",
1166            "RTRIM",
1167            "BTRIM",
1168            "CONCAT",
1169            "CONCAT_WS",
1170            "REVERSE",
1171            "LEFT",
1172            "RIGHT",
1173            "QUOTE_LITERAL",
1174            "ABS",
1175            "ROUND",
1176            "COALESCE",
1177            "STDDEV",
1178            "VARIANCE",
1179            "MEDIAN",
1180            "PERCENTILE",
1181            "GROUP_CONCAT",
1182            "STRING_AGG",
1183            "FIRST",
1184            "LAST",
1185            "ARRAY_AGG",
1186            "COUNT_DISTINCT",
1187            "MONEY",
1188            "MONEY_ASSET",
1189            "MONEY_MINOR",
1190            "MONEY_SCALE",
1191            "VERIFY_PASSWORD",
1192            "CAST",
1193            "CASE",
1194        ] {
1195            assert!(is_scalar_function(name), "{name}");
1196        }
1197        assert!(!is_scalar_function("NOT_A_FUNCTION"));
1198
1199        for name in [
1200            "COUNT",
1201            "AVG",
1202            "SUM",
1203            "MIN",
1204            "MAX",
1205            "STDDEV",
1206            "VARIANCE",
1207            "MEDIAN",
1208            "PERCENTILE",
1209            "GROUP_CONCAT",
1210            "STRING_AGG",
1211            "FIRST",
1212            "LAST",
1213            "ARRAY_AGG",
1214            "COUNT_DISTINCT",
1215        ] {
1216            assert!(is_aggregate_function(name), "{name}");
1217        }
1218        assert!(!is_aggregate_function("LOWER"));
1219
1220        assert_eq!(aggregate_token_name(&Token::Count), Some("COUNT"));
1221        assert_eq!(aggregate_token_name(&Token::Sum), Some("SUM"));
1222        assert_eq!(aggregate_token_name(&Token::Avg), Some("AVG"));
1223        assert_eq!(aggregate_token_name(&Token::Min), Some("MIN"));
1224        assert_eq!(aggregate_token_name(&Token::Max), Some("MAX"));
1225        assert_eq!(aggregate_token_name(&Token::First), Some("FIRST"));
1226        assert_eq!(aggregate_token_name(&Token::Last), Some("LAST"));
1227        assert_eq!(aggregate_token_name(&Token::Ident("COUNT".into())), None);
1228
1229        assert_eq!(scalar_token_name(&Token::Left), Some("LEFT"));
1230        assert_eq!(scalar_token_name(&Token::Right), Some("RIGHT"));
1231        assert_eq!(scalar_token_name(&Token::Ident("LEFT".into())), None);
1232
1233        for unit in [
1234            "ms",
1235            "msec",
1236            "millisecond",
1237            "milliseconds",
1238            "s",
1239            "sec",
1240            "secs",
1241            "second",
1242            "seconds",
1243            "m",
1244            "min",
1245            "mins",
1246            "minute",
1247            "minutes",
1248            "h",
1249            "hr",
1250            "hrs",
1251            "hour",
1252            "hours",
1253            "d",
1254            "day",
1255            "days",
1256        ] {
1257            assert!(is_duration_unit(unit), "{unit}");
1258        }
1259        assert!(!is_duration_unit("fortnight"));
1260    }
1261
1262    #[test]
1263    fn projection_and_group_render_helpers_cover_aliases_and_exprs() {
1264        let field = FieldRef::TableColumn {
1265            table: String::new(),
1266            column: "name".into(),
1267        };
1268        let filter = Filter::Compare {
1269            field: field.clone(),
1270            op: CompareOp::Eq,
1271            value: Value::text("alice"),
1272        };
1273
1274        assert_eq!(
1275            attach_projection_alias(Projection::Field(field.clone(), None), Some("n".into())),
1276            Projection::Field(field.clone(), Some("n".into()))
1277        );
1278        assert_eq!(
1279            attach_projection_alias(
1280                Projection::Expression(Box::new(filter.clone()), None),
1281                Some("ok".into())
1282            ),
1283            Projection::Expression(Box::new(filter), Some("ok".into()))
1284        );
1285        assert_eq!(
1286            attach_projection_alias(
1287                Projection::Function("LOWER".into(), vec![]),
1288                Some("l".into())
1289            ),
1290            Projection::Function("LOWER:l".into(), vec![])
1291        );
1292        assert_eq!(
1293            attach_projection_alias(
1294                Projection::Function("LOWER:l".into(), vec![]),
1295                Some("ignored".into())
1296            ),
1297            Projection::Function("LOWER:l".into(), vec![])
1298        );
1299        assert_eq!(
1300            attach_projection_alias(Projection::Column("name".into()), Some("n".into())),
1301            Projection::Alias("name".into(), "n".into())
1302        );
1303        assert_eq!(
1304            attach_projection_alias(Projection::All, Some("ignored".into())),
1305            Projection::All
1306        );
1307
1308        assert_eq!(render_group_by_expr(&col("dept")).as_deref(), Some("dept"));
1309        assert_eq!(
1310            render_group_by_expr(&Expr::Column {
1311                field: FieldRef::TableColumn {
1312                    table: "employees".into(),
1313                    column: "dept".into()
1314                },
1315                span: Span::synthetic()
1316            })
1317            .as_deref(),
1318            Some("employees.dept")
1319        );
1320        assert_eq!(
1321            render_group_by_expr(&Expr::Column {
1322                field: FieldRef::NodeId { alias: "n".into() },
1323                span: Span::synthetic()
1324            }),
1325            Some("NodeId { alias: \"n\" }".into())
1326        );
1327        assert_eq!(
1328            render_group_by_expr(&Expr::Literal {
1329                value: Value::Null,
1330                span: Span::synthetic()
1331            })
1332            .as_deref(),
1333            Some("")
1334        );
1335        assert_eq!(
1336            render_group_by_expr(&Expr::Literal {
1337                value: Value::text("5m"),
1338                span: Span::synthetic()
1339            })
1340            .as_deref(),
1341            Some("5m")
1342        );
1343        assert_eq!(
1344            render_group_by_expr(&Expr::Literal {
1345                value: Value::Integer(7),
1346                span: Span::synthetic()
1347            })
1348            .as_deref(),
1349            Some("7")
1350        );
1351        assert_eq!(
1352            render_group_by_expr(&Expr::FunctionCall {
1353                name: "TIME_BUCKET".into(),
1354                args: vec![
1355                    col("ts"),
1356                    Expr::Literal {
1357                        value: Value::text("5m"),
1358                        span: Span::synthetic()
1359                    }
1360                ],
1361                span: Span::synthetic()
1362            })
1363            .as_deref(),
1364            Some("TIME_BUCKET(ts,5m)")
1365        );
1366        assert_eq!(
1367            render_group_by_expr(&Expr::FunctionCall {
1368                name: "LOWER".into(),
1369                args: vec![col("dept")],
1370                span: Span::synthetic()
1371            })
1372            .as_deref(),
1373            Some("LOWER()")
1374        );
1375
1376        assert_eq!(
1377            render_group_by_function_arg(&Projection::Column("LIT:5m".into())),
1378            Some("5m".into())
1379        );
1380        assert_eq!(
1381            render_group_by_function_arg(&Projection::Column("dept".into())),
1382            Some("dept".into())
1383        );
1384        assert_eq!(
1385            render_group_by_function_arg(&Projection::All),
1386            Some("*".into())
1387        );
1388        assert_eq!(
1389            render_group_by_function_arg(&Projection::Function("LOWER".into(), vec![])),
1390            None
1391        );
1392    }
1393
1394    #[test]
1395    fn expression_aggregate_detection_branches() {
1396        let count = Expr::FunctionCall {
1397            name: "COUNT".into(),
1398            args: vec![col("id")],
1399            span: Span::synthetic(),
1400        };
1401        assert!(contains_nested_aggregate(&count));
1402        assert!(is_plain_aggregate_expr(&count));
1403
1404        let nested = Expr::FunctionCall {
1405            name: "SUM".into(),
1406            args: vec![count.clone()],
1407            span: Span::synthetic(),
1408        };
1409        assert!(contains_nested_aggregate(&nested));
1410        assert!(!is_plain_aggregate_expr(&nested));
1411
1412        let binary = Expr::BinaryOp {
1413            op: BinOp::Add,
1414            lhs: Box::new(col("a")),
1415            rhs: Box::new(count.clone()),
1416            span: Span::synthetic(),
1417        };
1418        assert!(contains_nested_aggregate(&binary));
1419
1420        let unary = Expr::UnaryOp {
1421            op: UnaryOp::Not,
1422            operand: Box::new(count.clone()),
1423            span: Span::synthetic(),
1424        };
1425        assert!(contains_nested_aggregate(&unary));
1426
1427        let cast = Expr::Cast {
1428            inner: Box::new(count.clone()),
1429            target: reddb_types::types::DataType::Integer,
1430            span: Span::synthetic(),
1431        };
1432        assert!(contains_nested_aggregate(&cast));
1433
1434        let case = Expr::Case {
1435            branches: vec![(col("flag"), count.clone())],
1436            else_: Some(Box::new(col("fallback"))),
1437            span: Span::synthetic(),
1438        };
1439        assert!(contains_nested_aggregate(&case));
1440
1441        let in_list = Expr::InList {
1442            target: Box::new(col("id")),
1443            values: vec![count.clone()],
1444            negated: false,
1445            span: Span::synthetic(),
1446        };
1447        assert!(contains_nested_aggregate(&in_list));
1448
1449        let between = Expr::Between {
1450            target: Box::new(col("id")),
1451            low: Box::new(col("low")),
1452            high: Box::new(count),
1453            negated: false,
1454            span: Span::synthetic(),
1455        };
1456        assert!(contains_nested_aggregate(&between));
1457        assert!(!contains_nested_aggregate(&Expr::Parameter {
1458            index: 1,
1459            span: Span::synthetic()
1460        }));
1461
1462        assert!(crate::parser::parse("SELECT SUM(COUNT(id)) FROM t").is_err());
1463    }
1464
1465    #[test]
1466    fn table_clause_parsing_covers_as_of_order_offset_and_expand() {
1467        let table = parse_table(
1468            "SELECT name FROM users AS OF COMMIT 'abc123' \
1469             WHERE deleted_at IS NULL \
1470             ORDER BY LOWER(name) ASC NULLS FIRST, created_at DESC NULLS LAST \
1471             LIMIT 10 OFFSET 5 WITH EXPAND GRAPH DEPTH 3, CROSS_REFS",
1472        );
1473        assert!(matches!(table.as_of, Some(AsOfClause::Commit(ref v)) if v == "abc123"));
1474        assert!(table.filter.is_some());
1475        assert_eq!(table.order_by.len(), 2);
1476        assert!(table.order_by[0].expr.is_some());
1477        assert!(table.order_by[0].ascending);
1478        assert!(table.order_by[0].nulls_first);
1479        assert!(!table.order_by[1].ascending);
1480        assert!(!table.order_by[1].nulls_first);
1481        assert_eq!(table.limit, Some(10));
1482        assert_eq!(table.offset, Some(5));
1483        assert!(matches!(
1484            table.expand,
1485            Some(ExpandOptions {
1486                graph: true,
1487                graph_depth: 3,
1488                cross_refs: true,
1489                ..
1490            })
1491        ));
1492
1493        let table = parse_table("SELECT * FROM users AS OF BRANCH 'main'");
1494        assert!(matches!(table.as_of, Some(AsOfClause::Branch(ref v)) if v == "main"));
1495
1496        let table = parse_table("SELECT * FROM users AS OF TAG 'v1'");
1497        assert!(matches!(table.as_of, Some(AsOfClause::Tag(ref v)) if v == "v1"));
1498
1499        let table = parse_table("SELECT * FROM users AS OF TIMESTAMP 1710000000000");
1500        assert!(matches!(
1501            table.as_of,
1502            Some(AsOfClause::TimestampMs(1_710_000_000_000))
1503        ));
1504
1505        let table = parse_table("SELECT * FROM users AS OF SNAPSHOT 42");
1506        assert!(matches!(table.as_of, Some(AsOfClause::Snapshot(42))));
1507
1508        let table = parse_table("SELECT * FROM users WITH EXPAND");
1509        assert!(matches!(
1510            table.expand,
1511            Some(ExpandOptions {
1512                graph: true,
1513                graph_depth: 1,
1514                cross_refs: true,
1515                ..
1516            })
1517        ));
1518
1519        assert!(crate::parser::parse("SELECT * FROM users AS OF SNAPSHOT -1").is_err());
1520        assert!(crate::parser::parse("SELECT * FROM users AS OF UNKNOWN 'x'").is_err());
1521    }
1522
1523    #[test]
1524    fn direct_parser_helpers_cover_projection_group_order_and_literals() {
1525        let mut parser = Parser::new("name, LOWER(email) AS email_l").unwrap();
1526        let projections = parser.parse_projection_list().unwrap();
1527        assert_eq!(projections.len(), 2);
1528
1529        let mut parser = Parser::new("dept, TIME_BUCKET(5 m)").unwrap();
1530        let group_by = parser.parse_group_by_list().unwrap();
1531        assert_eq!(group_by, vec!["dept", "TIME_BUCKET(5m)"]);
1532
1533        let mut parser = Parser::new("LOWER(name) DESC, created_at").unwrap();
1534        let order_by = parser.parse_order_by_list().unwrap();
1535        assert_eq!(order_by.len(), 2);
1536        assert!(order_by[0].expr.is_some());
1537        assert!(!order_by[0].ascending);
1538        assert!(order_by[0].nulls_first);
1539        assert!(order_by[1].ascending);
1540        assert!(!order_by[1].nulls_first);
1541
1542        let mut parser = Parser::new("-5 ms").unwrap();
1543        assert_eq!(parser.parse_function_literal_arg().unwrap(), "-5ms");
1544        let mut parser = Parser::new("2.0 H").unwrap();
1545        assert_eq!(parser.parse_function_literal_arg().unwrap(), "2h");
1546        let mut parser = Parser::new("bad").unwrap();
1547        assert!(parser.parse_function_literal_arg().is_err());
1548    }
1549
1550    #[test]
1551    fn from_subquery_source_is_preserved() {
1552        let parsed = crate::parser::parse("FROM (SELECT id FROM users) AS u RETURN u.id")
1553            .unwrap()
1554            .query;
1555        let QueryExpr::Table(table) = parsed else {
1556            panic!("expected table query");
1557        };
1558        assert_eq!(table.table, "__subq_u");
1559        assert_eq!(table.alias.as_deref(), Some("u"));
1560        assert!(matches!(table.source, Some(TableSource::Subquery(_))));
1561        assert_eq!(table.select_items.len(), 1);
1562
1563        let parsed = crate::parser::parse("SELECT id FROM (SELECT id FROM users) AS u")
1564            .unwrap()
1565            .query;
1566        let QueryExpr::Table(table) = parsed else {
1567            panic!("expected table query");
1568        };
1569        assert_eq!(table.table, "__subq_u");
1570        assert_eq!(table.alias.as_deref(), Some("u"));
1571        assert!(matches!(table.source, Some(TableSource::Subquery(_))));
1572        assert_eq!(table.select_items.len(), 1);
1573
1574        assert!(crate::parser::parse("FROM (MATCH (n) RETURN n) AS g").is_err());
1575        assert!(crate::parser::parse("SELECT * FROM (MATCH (n) RETURN n) AS g").is_err());
1576    }
1577
1578    // ── Table-valued function arguments (issues #795 / #796) ──
1579
1580    #[test]
1581    fn louvain_tvf_parses_positional_and_named_args() {
1582        // Bare positional form: louvain(<graph>).
1583        let table = parse_table("SELECT * FROM louvain(g)");
1584        match table.source {
1585            Some(TableSource::Function {
1586                ref name,
1587                ref args,
1588                ref named_args,
1589            }) => {
1590                assert_eq!(name, "louvain");
1591                assert_eq!(args, &vec!["g".to_string()]);
1592                assert!(named_args.is_empty());
1593            }
1594            other => panic!("expected louvain TVF source, got {other:?}"),
1595        }
1596
1597        // Named-argument form: louvain(<graph>, resolution => <f64>).
1598        let table = parse_table("SELECT * FROM louvain(g, resolution => 0.5)");
1599        match table.source {
1600            Some(TableSource::Function {
1601                ref name,
1602                ref args,
1603                ref named_args,
1604            }) => {
1605                assert_eq!(name, "louvain");
1606                assert_eq!(args, &vec!["g".to_string()]);
1607                assert_eq!(named_args.len(), 1);
1608                assert_eq!(named_args[0].0, "resolution");
1609                assert!((named_args[0].1 - 0.5).abs() < f64::EPSILON);
1610            }
1611            other => panic!("expected louvain TVF source, got {other:?}"),
1612        }
1613
1614        // Integer resolution is accepted and coerced to f64.
1615        let table = parse_table("SELECT * FROM louvain(g, resolution => 2)");
1616        match table.source {
1617            Some(TableSource::Function { ref named_args, .. }) => {
1618                assert!((named_args[0].1 - 2.0).abs() < f64::EPSILON);
1619            }
1620            other => panic!("expected louvain TVF source, got {other:?}"),
1621        }
1622    }
1623
1624    // ── Inline graph TVF: `nodes => / edges =>` subqueries (issue #799) ──
1625
1626    #[test]
1627    fn tvf_inline_form_parses_nodes_and_edges_subqueries() {
1628        // The inline form must produce a structurally distinct AST node
1629        // (InlineGraphFunction) from the graph-collection Function form.
1630        let table = parse_table(
1631            "SELECT * FROM components(nodes => (SELECT id FROM hosts), edges => (SELECT src, dst FROM links))",
1632        );
1633        match table.source {
1634            Some(TableSource::InlineGraphFunction {
1635                ref name,
1636                ref nodes,
1637                ref edges,
1638                ref named_args,
1639            }) => {
1640                assert_eq!(name, "components");
1641                assert!(named_args.is_empty());
1642                assert!(matches!(**nodes, QueryExpr::Table(_)));
1643                assert!(matches!(**edges, QueryExpr::Table(_)));
1644            }
1645            other => panic!("expected inline graph TVF source, got {other:?}"),
1646        }
1647    }
1648
1649    #[test]
1650    fn tvf_inline_form_carries_numeric_named_args() {
1651        // `resolution => <f64>` coexists with the inline subqueries.
1652        let table = parse_table(
1653            "SELECT * FROM louvain(nodes => (SELECT id FROM n), edges => (SELECT a, b FROM e), resolution => 0.5)",
1654        );
1655        match table.source {
1656            Some(TableSource::InlineGraphFunction {
1657                ref name,
1658                ref named_args,
1659                ..
1660            }) => {
1661                assert_eq!(name, "louvain");
1662                assert_eq!(named_args.len(), 1);
1663                assert_eq!(named_args[0].0, "resolution");
1664                assert!((named_args[0].1 - 0.5).abs() < f64::EPSILON);
1665            }
1666            other => panic!("expected inline graph TVF source, got {other:?}"),
1667        }
1668    }
1669
1670    #[test]
1671    fn tvf_inline_form_rejects_malformed_shapes() {
1672        // A positional graph argument cannot mix with inline subqueries.
1673        assert!(crate::parser::parse(
1674            "SELECT * FROM components(g, nodes => (SELECT id FROM n), edges => (SELECT a, b FROM e))"
1675        )
1676        .is_err());
1677        // The inline form requires both `nodes` and `edges`.
1678        assert!(
1679            crate::parser::parse("SELECT * FROM components(nodes => (SELECT id FROM n))").is_err()
1680        );
1681        assert!(
1682            crate::parser::parse("SELECT * FROM components(edges => (SELECT a, b FROM e))")
1683                .is_err()
1684        );
1685        // An unknown subquery key is rejected.
1686        assert!(crate::parser::parse(
1687            "SELECT * FROM components(nodes => (SELECT id FROM n), verts => (SELECT a, b FROM e))"
1688        )
1689        .is_err());
1690        // A `=>` followed by a non-SELECT parenthesised group is rejected.
1691        assert!(crate::parser::parse(
1692            "SELECT * FROM components(nodes => (1 + 2), edges => (SELECT a, b FROM e))"
1693        )
1694        .is_err());
1695    }
1696
1697    #[test]
1698    fn shortest_path_tvf_parses_graph_ref_with_scalar_named_args() {
1699        // Required src/dst only.
1700        let table = parse_table("SELECT * FROM shortest_path(g, src => 1, dst => 4)");
1701        match table.source {
1702            Some(TableSource::Function {
1703                ref name,
1704                ref args,
1705                ref named_args,
1706            }) => {
1707                assert_eq!(name, "shortest_path");
1708                assert_eq!(args, &vec!["g".to_string()]);
1709                assert_eq!(named_args.len(), 2);
1710                assert_eq!(named_args[0].0, "src");
1711                assert!((named_args[0].1 - 1.0).abs() < f64::EPSILON);
1712                assert_eq!(named_args[1].0, "dst");
1713                assert!((named_args[1].1 - 4.0).abs() < f64::EPSILON);
1714            }
1715            other => panic!("expected shortest_path TVF source, got {other:?}"),
1716        }
1717
1718        // Optional max_hops named argument is accepted alongside src/dst.
1719        let table =
1720            parse_table("SELECT * FROM shortest_path(g, src => 1, dst => 4, max_hops => 3)");
1721        match table.source {
1722            Some(TableSource::Function { ref named_args, .. }) => {
1723                assert_eq!(named_args.len(), 3);
1724                assert_eq!(named_args[2].0, "max_hops");
1725                assert!((named_args[2].1 - 3.0).abs() < f64::EPSILON);
1726            }
1727            other => panic!("expected shortest_path TVF source, got {other:?}"),
1728        }
1729    }
1730
1731    #[test]
1732    fn centrality_tvfs_parse_positional_and_named_args() {
1733        // Bare positional form for each centrality TVF (issue #797). These flow
1734        // through the generic `ident(args)` path (not a dedicated keyword), so
1735        // the parser records them as a `TableSource::Function`.
1736        for name in ["betweenness", "eigenvector", "pagerank"] {
1737            let table = parse_table(&format!("SELECT * FROM {name}(g)"));
1738            match table.source {
1739                Some(TableSource::Function {
1740                    name: ref got,
1741                    ref args,
1742                    ref named_args,
1743                }) => {
1744                    assert_eq!(got, name);
1745                    assert_eq!(args, &vec!["g".to_string()]);
1746                    assert!(named_args.is_empty());
1747                }
1748                other => panic!("expected {name} TVF source, got {other:?}"),
1749            }
1750        }
1751
1752        // eigenvector(<graph>, max_iterations => <i64>, tolerance => <f64>).
1753        let table =
1754            parse_table("SELECT * FROM eigenvector(g, max_iterations => 50, tolerance => 0.0001)");
1755        match table.source {
1756            Some(TableSource::Function { ref named_args, .. }) => {
1757                assert_eq!(named_args.len(), 2);
1758                assert_eq!(named_args[0].0, "max_iterations");
1759                assert!((named_args[0].1 - 50.0).abs() < f64::EPSILON);
1760                assert_eq!(named_args[1].0, "tolerance");
1761                assert!((named_args[1].1 - 0.0001).abs() < f64::EPSILON);
1762            }
1763            other => panic!("expected eigenvector TVF source, got {other:?}"),
1764        }
1765
1766        // pagerank(<graph>, damping => <f64>, max_iterations => <i64>).
1767        let table =
1768            parse_table("SELECT * FROM pagerank(g, damping => 0.85, max_iterations => 100)");
1769        match table.source {
1770            Some(TableSource::Function {
1771                ref args,
1772                ref named_args,
1773                ..
1774            }) => {
1775                assert_eq!(args, &vec!["g".to_string()]);
1776                assert_eq!(named_args.len(), 2);
1777                assert_eq!(named_args[0].0, "damping");
1778                assert!((named_args[0].1 - 0.85).abs() < f64::EPSILON);
1779                assert_eq!(named_args[1].0, "max_iterations");
1780                assert!((named_args[1].1 - 100.0).abs() < f64::EPSILON);
1781            }
1782            other => panic!("expected pagerank TVF source, got {other:?}"),
1783        }
1784    }
1785
1786    #[test]
1787    fn tvf_named_arg_grammar_rejects_malformed_forms() {
1788        // A positional argument after a named argument is rejected.
1789        assert!(crate::parser::parse("SELECT * FROM louvain(g, resolution => 0.5, h)").is_err());
1790        // `=>` must be followed by a number.
1791        assert!(crate::parser::parse("SELECT * FROM louvain(g, resolution => foo)").is_err());
1792        // Zero-argument form is still rejected (issue #795 invariant).
1793        assert!(crate::parser::parse("SELECT * FROM louvain()").is_err());
1794    }
1795
1796    // ── SESSIONIZE operator (issue #585 slice 8) ──
1797
1798    #[test]
1799    fn test_parse_sessionize_full_clause() {
1800        let q = parse_table(
1801            "SELECT user_id, ts FROM events SESSIONIZE BY user_id GAP 30 m ORDER BY ts",
1802        );
1803        let s = q.sessionize.expect("sessionize present");
1804        assert_eq!(s.actor_col.as_deref(), Some("user_id"));
1805        assert_eq!(s.gap_ms, Some(30 * 60_000));
1806        assert_eq!(s.order_col.as_deref(), Some("ts"));
1807    }
1808
1809    #[test]
1810    fn test_parse_sessionize_omits_optional_order_by() {
1811        let q = parse_table("SELECT * FROM events SESSIONIZE BY user_id GAP 5 s");
1812        let s = q.sessionize.expect("sessionize present");
1813        assert_eq!(s.actor_col.as_deref(), Some("user_id"));
1814        assert_eq!(s.gap_ms, Some(5_000));
1815        assert!(s.order_col.is_none());
1816    }
1817
1818    #[test]
1819    fn test_parse_sessionize_bare_defers_to_descriptor() {
1820        // Both BY and GAP omitted — parser accepts the shape; the
1821        // executor raises MissingSessionKey when the descriptor
1822        // doesn't supply defaults.
1823        let q = parse_table("SELECT * FROM events SESSIONIZE");
1824        let s = q.sessionize.expect("sessionize present");
1825        assert!(s.actor_col.is_none());
1826        assert!(s.gap_ms.is_none());
1827        assert!(s.order_col.is_none());
1828    }
1829
1830    #[test]
1831    fn test_parse_sessionize_composes_with_where_and_limit() {
1832        let q = parse_table(
1833            "SELECT user_id FROM events \
1834             SESSIONIZE BY user_id GAP 1 m \
1835             WHERE user_id = 'u1' LIMIT 10",
1836        );
1837        let s = q.sessionize.expect("sessionize present");
1838        assert_eq!(s.actor_col.as_deref(), Some("user_id"));
1839        assert_eq!(s.gap_ms, Some(60_000));
1840        assert!(q.where_expr.is_some(), "WHERE still parsed");
1841        assert_eq!(q.limit, Some(10));
1842    }
1843
1844    #[test]
1845    fn test_parse_sessionize_absent_leaves_field_none() {
1846        let q = parse_table("SELECT * FROM events");
1847        assert!(q.sessionize.is_none());
1848    }
1849
1850    #[test]
1851    fn test_parse_sessionize_with_session_id_in_projection_e2e_shape() {
1852        // Matches the literal shape e2e tests use — session_id in the
1853        // projection list must not confuse the parser.
1854        let q = parse_table(
1855            "SELECT id, user_id, ts, session_id FROM events \
1856             SESSIONIZE BY user_id GAP 30 s ORDER BY ts",
1857        );
1858        let s = q.sessionize.expect("sessionize present");
1859        assert_eq!(s.actor_col.as_deref(), Some("user_id"));
1860        assert_eq!(s.gap_ms, Some(30_000));
1861    }
1862}