Skip to main content

reddb_rql/parser/
expr.rs

1//! Pratt-style parser for the Fase 2 `Expr` AST.
2//!
3//! This module is the Week 2 deliverable of the parser v2 refactor
4//! tracked in `/home/cyber/.claude/plans/squishy-mixing-honey.md`.
5//! It produces `ast::Expr` trees with proper operator precedence,
6//! `Span` tracking from the lexer, and support for the full set of
7//! unary / binary / postfix operators the existing hand-rolled
8//! projection climb covers in Fase 1.3 — plus the missing pieces
9//! (CASE, CAST, parenthesised subexprs, IS NULL, IN, BETWEEN).
10//!
11//! # Design notes
12//!
13//! The parser is now the canonical entry point for SQL expression
14//! parsing in the table-query flow:
15//! - `SELECT` projections parse through `Parser::parse_expr`
16//! - `WHERE` / `HAVING` operands parse through `Parser::parse_expr`
17//! - `ORDER BY` expressions parse through `Parser::parse_expr`
18//!
19//! Some legacy AST slots are still adapter-based (`Projection`,
20//! `Filter`, `GROUP BY` strings), so statement parsing still lowers
21//! `Expr` trees into those older shapes at the boundary.
22//!
23//! # Precedence table (matches PG gram.y modulo features we don't have)
24//!
25//! ```text
26//! prec  operators
27//! ----  ----------------------------------
28//!  10   OR
29//!  20   AND
30//!  25   NOT                      (prefix)
31//!  30   = <> < <= > >=           (comparison)
32//!  32   IS NULL / IS NOT NULL    (postfix)
33//!  33   BETWEEN … AND …          (postfix)
34//!  34   IN (…)                   (postfix)
35//!  40   ||                       (string concat)
36//!  50   + -                      (additive)
37//!  60   * / %                    (multiplicative)
38//!  70   -                        (unary negation)
39//!  80   ::type  CAST(…AS type)   (explicit type coercion)
40//! ```
41//!
42//! Higher precedence binds tighter. The climb uses the classic
43//! "min-precedence" algorithm — `parse_expr_prec(min)` loops consuming
44//! any infix operator whose precedence is ≥ `min`, recursing with
45//! `prec + 1` on the right-hand side for left-associativity.
46
47use super::error::ParseError;
48use super::Parser;
49use super::PlaceholderMode;
50use crate::ast::{BinOp, Expr, ExprSubquery, FieldRef, Span, UnaryOp};
51use crate::lexer::Token;
52use reddb_types::types::{DataType, Value};
53
54fn is_duration_unit(unit: &str) -> bool {
55    matches!(
56        unit.to_ascii_lowercase().as_str(),
57        "ms" | "msec"
58            | "millisecond"
59            | "milliseconds"
60            | "s"
61            | "sec"
62            | "secs"
63            | "second"
64            | "seconds"
65            | "m"
66            | "min"
67            | "mins"
68            | "minute"
69            | "minutes"
70            | "h"
71            | "hr"
72            | "hrs"
73            | "hour"
74            | "hours"
75            | "d"
76            | "day"
77            | "days"
78    )
79}
80
81fn keyword_function_name(token: &Token) -> Option<&'static str> {
82    match token {
83        Token::Count => Some("COUNT"),
84        Token::Sum => Some("SUM"),
85        Token::Avg => Some("AVG"),
86        Token::Min => Some("MIN"),
87        Token::Max => Some("MAX"),
88        Token::First => Some("FIRST"),
89        Token::Last => Some("LAST"),
90        Token::Left => Some("LEFT"),
91        Token::Right => Some("RIGHT"),
92        Token::Contains => Some("CONTAINS"),
93        Token::Kv => Some("KV"),
94        _ => None,
95    }
96}
97
98/// Whether `name` may appear as the function in `fn(...) OVER (...)`.
99/// Window-only functions plus the standard aggregates (which behave as
100/// window aggregates when an OVER clause is attached). Mirrored loosely
101/// from PG's pg_proc catalog — slice 7a only validates lexical eligibility,
102/// runtime support arrives with the analytics executor.
103fn is_window_eligible_function(name: &str) -> bool {
104    matches!(
105        name.to_ascii_uppercase().as_str(),
106        // Window-only.
107        "LAG"
108            | "LEAD"
109            | "ROW_NUMBER"
110            | "RANK"
111            | "DENSE_RANK"
112            | "PERCENT_RANK"
113            | "CUME_DIST"
114            | "NTILE"
115            | "FIRST_VALUE"
116            | "LAST_VALUE"
117            | "NTH_VALUE"
118            // Aggregates valid in window position.
119            | "COUNT"
120            | "SUM"
121            | "AVG"
122            | "MIN"
123            | "MAX"
124            | "STDDEV"
125            | "VARIANCE"
126            | "MEDIAN"
127            | "PERCENTILE"
128            | "GROUP_CONCAT"
129            | "STRING_AGG"
130            | "FIRST"
131            | "LAST"
132            | "ARRAY_AGG"
133            | "COUNT_DISTINCT"
134    )
135}
136
137fn bare_zero_arg_function_name(name: &str) -> Option<&'static str> {
138    match name.to_ascii_uppercase().as_str() {
139        "CURRENT_TIMESTAMP" => Some("CURRENT_TIMESTAMP"),
140        "CURRENT_DATE" => Some("CURRENT_DATE"),
141        "CURRENT_TIME" => Some("CURRENT_TIME"),
142        _ => None,
143    }
144}
145
146impl<'a> Parser<'a> {
147    /// Parse a complete expression at the lowest precedence level.
148    /// Entry point for every caller that wants an `Expr` tree.
149    pub fn parse_expr(&mut self) -> Result<Expr, ParseError> {
150        self.parse_expr_prec(0)
151    }
152
153    pub(crate) fn parse_expr_with_min_precedence(
154        &mut self,
155        min_prec: u8,
156    ) -> Result<Expr, ParseError> {
157        self.parse_expr_prec(min_prec)
158    }
159
160    /// Continue parsing an expression after the caller has already
161    /// materialized the left-hand side atom.
162    pub(crate) fn continue_expr(&mut self, left: Expr, min_prec: u8) -> Result<Expr, ParseError> {
163        self.parse_expr_suffix(left, min_prec)
164    }
165
166    /// Pratt climb: parse a unary atom then consume any infix operators
167    /// whose precedence meets or exceeds `min_prec`.
168    fn parse_expr_prec(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
169        // Depth guard: every recursive descent point in the expr
170        // grammar bottoms out here, so checking once is enough to
171        // catch deeply nested literals like `((((((1))))))` and
172        // boolean chains like `NOT NOT NOT NOT … x`.
173        self.enter_depth()?;
174        let result = (|| {
175            let left = self.parse_expr_unary()?;
176            self.parse_expr_suffix(left, min_prec)
177        })();
178        self.exit_depth();
179        result
180    }
181
182    fn parse_expr_suffix(&mut self, mut left: Expr, min_prec: u8) -> Result<Expr, ParseError> {
183        let max_infix_chain = self.depth.max_depth.saturating_mul(8).max(1);
184        let mut infix_chain = 0usize;
185        loop {
186            let Some((op, prec)) = self.peek_binop() else {
187                // Not a standard infix op — check for postfix forms.
188                if min_prec <= 32 {
189                    if let Some(node) = self.try_parse_postfix(&left)? {
190                        left = node;
191                        continue;
192                    }
193                }
194                // Didactic (#1704): `->` / `->>` are Postgres JSON operators a
195                // document-model newcomer predictably reaches for. Teach the
196                // dotted-path idiom instead of leaving a dangling `->` for the
197                // caller to reject as a generic "Unexpected token". Graph-mode
198                // traversal consumes `->` / `<-` in graph.rs before it ever
199                // reaches this scalar-expression path, so it is unaffected.
200                if matches!(self.peek(), Token::Arrow) {
201                    return Err(ParseError::json_arrow_operator(self.position()));
202                }
203                break;
204            };
205            if prec < min_prec {
206                break;
207            }
208            infix_chain += 1;
209            if infix_chain > max_infix_chain {
210                return Err(ParseError::token_limit(
211                    "max_tokens",
212                    self.max_tokens,
213                    self.position(),
214                ));
215            }
216            self.advance()?; // consume the operator token
217            let start_span = self.span_start_of(&left);
218            let rhs = self.parse_expr_prec(prec + 1)?;
219            let end_span = self.span_end_of(&rhs);
220            left = Expr::BinaryOp {
221                op,
222                lhs: Box::new(left),
223                rhs: Box::new(rhs),
224                span: Span::new(start_span, end_span),
225            };
226        }
227        Ok(left)
228    }
229
230    /// Parse a unary-prefix expression or drop through to the atomic
231    /// factor. Handles `NOT`, unary `-`, and `+` (no-op sign).
232    fn parse_expr_unary(&mut self) -> Result<Expr, ParseError> {
233        match self.peek() {
234            Token::Not => {
235                let start = self.position();
236                self.advance()?;
237                let operand = self.parse_expr_prec(25)?;
238                let end = self.span_end_of(&operand);
239                Ok(Expr::UnaryOp {
240                    op: UnaryOp::Not,
241                    operand: Box::new(operand),
242                    span: Span::new(start, end),
243                })
244            }
245            Token::Dash => {
246                let start = self.position();
247                self.advance()?;
248                let operand = self.parse_expr_prec(70)?;
249                let end = self.span_end_of(&operand);
250                Ok(Expr::UnaryOp {
251                    op: UnaryOp::Neg,
252                    operand: Box::new(operand),
253                    span: Span::new(start, end),
254                })
255            }
256            Token::Plus => {
257                // Unary plus is a no-op. Consume and recurse.
258                self.advance()?;
259                self.parse_expr_prec(70)
260            }
261            _ => self.parse_expr_factor(),
262        }
263    }
264
265    /// Parse a single atomic expression factor: literal, column ref,
266    /// parenthesised subexpression, CAST, CASE, or function call.
267    fn parse_expr_factor(&mut self) -> Result<Expr, ParseError> {
268        let start = self.position();
269
270        // Parenthesised subexpression: `( expr )`
271        if self.consume(&Token::LParen)? {
272            if self.check(&Token::Select) {
273                let query = self.parse_select_query()?;
274                self.expect(Token::RParen)?;
275                return Ok(Expr::Subquery {
276                    query: ExprSubquery {
277                        query: Box::new(query),
278                    },
279                    span: Span::new(start, self.position()),
280                });
281            }
282            let inner = self.parse_expr_prec(0)?;
283            self.expect(Token::RParen)?;
284            return Ok(inner);
285        }
286
287        // Literal: true / false / null
288        if self.consume(&Token::True)? {
289            return Ok(Expr::Literal {
290                value: Value::Boolean(true),
291                span: Span::new(start, self.position()),
292            });
293        }
294        if self.consume(&Token::False)? {
295            return Ok(Expr::Literal {
296                value: Value::Boolean(false),
297                span: Span::new(start, self.position()),
298            });
299        }
300        if self.consume(&Token::Null)? {
301            return Ok(Expr::Literal {
302                value: Value::Null,
303                span: Span::new(start, self.position()),
304            });
305        }
306
307        // Numeric literals — with optional duration-unit suffix (e.g. `5m`, `10s`, `2h`).
308        // Duration literals are emitted as Value::Text so downstream code sees "5m" verbatim
309        // (matching the legacy Projection::Column("LIT:5m") path used by time_bucket).
310        if let Token::Integer(n) = *self.peek() {
311            self.advance()?;
312            if let Token::Ident(ref unit) = *self.peek() {
313                if is_duration_unit(unit) {
314                    let duration = format!("{n}{}", unit.to_ascii_lowercase());
315                    self.advance()?;
316                    return Ok(Expr::Literal {
317                        value: Value::text(duration),
318                        span: Span::new(start, self.position()),
319                    });
320                }
321            }
322            return Ok(Expr::Literal {
323                value: Value::Integer(n),
324                span: Span::new(start, self.position()),
325            });
326        }
327        if let Token::Float(n) = *self.peek() {
328            self.advance()?;
329            return Ok(Expr::Literal {
330                value: Value::Float(n),
331                span: Span::new(start, self.position()),
332            });
333        }
334        if let Token::String(ref s) = *self.peek() {
335            let text = s.clone();
336            self.advance()?;
337            return Ok(Expr::Literal {
338                value: Value::text(text),
339                span: Span::new(start, self.position()),
340            });
341        }
342
343        // JSON object `{…}` and array `[…]` literals — delegate to the DML literal parser
344        // which already handles the full JSON value grammar including nested objects.
345        // `JsonLiteral` is the strict-JSON variant emitted by the lexer's sub-mode
346        // when `{` is followed by `"`; both shapes route through `parse_literal_value`.
347        if matches!(
348            self.peek(),
349            Token::LBrace | Token::LBracket | Token::JsonLiteral(_)
350        ) {
351            let value = self
352                .parse_literal_value()
353                .map_err(|e| ParseError::new(e.message, self.position()))?;
354            return Ok(Expr::Literal {
355                value,
356                span: Span::new(start, self.position()),
357            });
358        }
359
360        // `?` positional placeholder — auto-numbered left-to-right.
361        // Immediate `?N` uses an explicit 1-based index. Mixing with
362        // `$N` in one statement is rejected.
363        if self.check(&Token::Question) {
364            let (index, span) = self.parse_question_param_index()?;
365            return Ok(Expr::Parameter { index, span });
366        }
367
368        if self.consume(&Token::Dollar)? {
369            // `$N` positional parameter placeholder (1-based in source,
370            // 0-based in the AST so it matches `Vec<Value>` indexing).
371            // Rejected at parse time when N < 1; gaps and arity are
372            // validated by the binder once the full statement is parsed.
373            if let Token::Integer(n) = *self.peek() {
374                if n < 1 {
375                    return Err(ParseError::new(
376                        "placeholder index must be >= 1".to_string(),
377                        self.position(),
378                    ));
379                }
380                if self.placeholder_mode == PlaceholderMode::Question {
381                    return Err(ParseError::new(
382                        "cannot mix `?` and `$N` placeholders in one statement".to_string(),
383                        self.position(),
384                    ));
385                }
386                self.placeholder_mode = PlaceholderMode::Dollar;
387                self.advance()?;
388                return Ok(Expr::Parameter {
389                    index: (n - 1) as usize,
390                    span: Span::new(start, self.position()),
391                });
392            }
393            let path = self.parse_dollar_ref_path()?;
394            let path_lc = path.to_ascii_lowercase();
395            let (name, key) = if let Some(rest) = path_lc.strip_prefix("secret.") {
396                ("__SECRET_REF", format!("red.vault/{rest}"))
397            } else if let Some(rest) = path_lc
398                .strip_prefix("red.secret.")
399                .or_else(|| path_lc.strip_prefix("red.secrets."))
400            {
401                ("__SECRET_REF", format!("red.vault/{rest}"))
402            } else if let Some(rest) = path_lc.strip_prefix("config.") {
403                ("CONFIG", format!("red.config/{rest}"))
404            } else if path_lc.starts_with("red.config.") {
405                let rest = path_lc.trim_start_matches("red.config.");
406                ("CONFIG", format!("red.config/{rest}"))
407            } else if let Some(rest) = path_lc
408                .strip_prefix("kv.")
409                .or_else(|| path_lc.strip_prefix("red.kv."))
410            {
411                // `$kv.<path>` resolves a plain (non-encrypted) user KV
412                // entry — sibling to `$secret.*`, distinct backing store
413                // (#1602). Desugars to `__KV_REF("red.kv/<path>")`.
414                ("__KV_REF", format!("red.kv/{rest}"))
415            } else {
416                return Err(ParseError::new(
417                    format!(
418                        "unknown $ reference `${path}`; expected $secret.*, $red.secret.*, $red.secrets.*, $config.*, $red.config.*, $kv.*, or $red.kv.*"
419                    ),
420                    self.position(),
421                ));
422            };
423            return Ok(Expr::FunctionCall {
424                name: name.to_string(),
425                args: vec![Expr::Literal {
426                    value: Value::text(key),
427                    span: Span::new(start, self.position()),
428                }],
429                span: Span::new(start, self.position()),
430            });
431        }
432
433        if let Some(name) = keyword_function_name(self.peek()) {
434            if matches!(self.peek_next()?, Token::LParen) {
435                self.advance()?; // consume the keyword token
436                return self.parse_function_call_expr_with_name(start, name.to_string());
437            }
438        }
439
440        // Identifier-led constructs: function call, CAST, CASE, column.
441        //
442        // We commit to consuming the identifier immediately and then
443        // inspect the NEXT token to decide shape. This avoids needing
444        // two-token lookahead on the parser. If the next token is `(`
445        // it's a function call; if `.` it's a qualified column ref;
446        // otherwise it's a bare column ref.
447        if let Token::Ident(ref name) = *self.peek() {
448            let name_upper = name.to_uppercase();
449
450            // CAST(expr AS type) — must test before consuming because
451            // CAST is not a reserved keyword; users could legitimately
452            // have a column literally named `cast`. Distinguish by
453            // looking at whether the identifier equals CAST AND is
454            // immediately followed by `(`. Since we can't two-step
455            // lookahead, handle CAST by parsing the ident, then if the
456            // uppercased name is CAST and the next token is `(`,
457            // switch to the CAST form; otherwise the saved name
458            // becomes the first segment of a column ref.
459            if name_upper == "CASE" {
460                return self.parse_case_expr(start);
461            }
462
463            let saved_name = name.clone();
464            self.advance()?; // consume the identifier unconditionally
465
466            // Function call / CAST: IDENT (
467            if matches!(self.peek(), Token::LParen) {
468                return self.parse_function_call_expr_with_name(start, saved_name);
469            }
470
471            if let Some(function_name) = bare_zero_arg_function_name(&saved_name) {
472                let end = self.position();
473                return Ok(Expr::FunctionCall {
474                    name: function_name.to_string(),
475                    args: Vec::new(),
476                    span: Span::new(start, end),
477                });
478            }
479
480            // Qualified column or dotted function: IDENT.IDENT[.IDENT …]
481            if matches!(self.peek(), Token::Dot) {
482                let mut segments = vec![saved_name];
483                while self.consume(&Token::Dot)? {
484                    segments.push(self.expect_ident_or_keyword()?);
485                }
486                if matches!(self.peek(), Token::LParen) {
487                    return self.parse_function_call_expr_with_name(start, segments.join("."));
488                }
489                let field = FieldRef::TableColumn {
490                    table: segments.remove(0),
491                    column: segments.join("."),
492                };
493                let end = self.position();
494                return Ok(Expr::Column {
495                    field,
496                    span: Span::new(start, end),
497                });
498            }
499
500            // Bare column reference with empty table name.
501            let field = FieldRef::TableColumn {
502                table: String::new(),
503                column: saved_name,
504            };
505            let end = self.position();
506            return Ok(Expr::Column {
507                field,
508                span: Span::new(start, end),
509            });
510        }
511
512        // Default: column reference (optionally qualified: table.column).
513        // Reached only when the leading token is not an Ident. Falls
514        // through to parse_field_ref which handles keyword-shaped
515        // column names.
516        let field = self.parse_field_ref()?;
517        let end = self.position();
518        Ok(Expr::Column {
519            field,
520            span: Span::new(start, end),
521        })
522    }
523
524    fn parse_dollar_ref_path(&mut self) -> Result<String, ParseError> {
525        let mut path = self.expect_ident_or_keyword()?;
526        while self.consume(&Token::Dot)? {
527            let next = self.expect_ident_or_keyword()?;
528            path = format!("{path}.{next}");
529        }
530        Ok(path)
531    }
532
533    fn parse_function_call_expr_with_name(
534        &mut self,
535        start: crate::lexer::Position,
536        function_name: String,
537    ) -> Result<Expr, ParseError> {
538        let call = self.parse_function_call_expr_with_name_inner(start, function_name)?;
539        // Issue #589 slice 7a: `fn(args) OVER (...)` lifts a plain
540        // FunctionCall into a WindowFunctionCall carrying the OVER
541        // clause. CAST and other shapes that don't return a
542        // FunctionCall are rejected by `parse_over_clause_for` so the
543        // user gets a clear error rather than silent acceptance.
544        if matches!(self.peek(), Token::Over) {
545            return self.lift_to_window_call(start, call);
546        }
547        Ok(call)
548    }
549
550    fn parse_function_call_expr_with_name_inner(
551        &mut self,
552        start: crate::lexer::Position,
553        function_name: String,
554    ) -> Result<Expr, ParseError> {
555        self.expect(Token::LParen)?;
556
557        if function_name.eq_ignore_ascii_case("CAST") {
558            let inner = self.parse_expr_prec(0)?;
559            self.expect(Token::As)?;
560            let type_name = self.expect_ident_or_keyword()?;
561            self.expect(Token::RParen)?;
562            let end = self.position();
563            let Some(target) = DataType::from_sql_name(&type_name) else {
564                return Err(ParseError::new(
565                    // F-05: `type_name` is caller-controlled identifier text.
566                    // Render via `{:?}` so embedded CR/LF/NUL/quotes are
567                    // escaped before reaching downstream serialization sinks.
568                    format!("unknown type name {type_name:?} in CAST"),
569                    self.position(),
570                ));
571            };
572            return Ok(Expr::Cast {
573                inner: Box::new(inner),
574                target,
575                span: Span::new(start, end),
576            });
577        }
578
579        // `GEO_WITHIN(<geo-column>, POLYGON(...))` — the composable form
580        // of the `SEARCH SPATIAL WITHIN POLYGON` verb. The first argument
581        // names a geo column and is captured through the spatial-column
582        // segment reader so a document field spelled `current` survives
583        // the window-frame keyword token instead of folding to `CURRENT`.
584        if function_name.eq_ignore_ascii_case("GEO_WITHIN") {
585            let column = self.parse_geo_column_expr()?;
586            self.expect(Token::Comma)?;
587            let polygon = self.parse_expr_prec(0)?;
588            self.expect(Token::RParen)?;
589            let end = self.position();
590            return Ok(Expr::FunctionCall {
591                name: function_name,
592                args: vec![column, polygon],
593                span: Span::new(start, end),
594            });
595        }
596
597        // `POLYGON((lat lon), (lat lon), …)` — vertices are space-separated
598        // pairs, matching the verb's spelling. The generic expression
599        // grammar would read `(38.70 -77.20)` as the subtraction
600        // `38.70 - 77.20`, so each coordinate parses at a precedence above
601        // the additive operators: an atom, optionally sign-prefixed, and
602        // nothing more.
603        if function_name.eq_ignore_ascii_case("POLYGON") {
604            let args = self.parse_polygon_vertex_args()?;
605            self.expect(Token::RParen)?;
606            let end = self.position();
607            return Ok(Expr::FunctionCall {
608                name: function_name,
609                args,
610                span: Span::new(start, end),
611            });
612        }
613
614        if function_name.eq_ignore_ascii_case("TRIM") {
615            let (name, args) = self.parse_trim_expr_args()?;
616            self.expect(Token::RParen)?;
617            let end = self.position();
618            return Ok(Expr::FunctionCall {
619                name,
620                args,
621                span: Span::new(start, end),
622            });
623        }
624
625        if function_name.eq_ignore_ascii_case("POSITION") {
626            let args = self.parse_position_expr_args()?;
627            self.expect(Token::RParen)?;
628            let end = self.position();
629            return Ok(Expr::FunctionCall {
630                name: function_name,
631                args,
632                span: Span::new(start, end),
633            });
634        }
635
636        if function_name.eq_ignore_ascii_case("SUBSTRING") {
637            let args = self.parse_substring_expr_args()?;
638            self.expect(Token::RParen)?;
639            let end = self.position();
640            return Ok(Expr::FunctionCall {
641                name: function_name,
642                args,
643                span: Span::new(start, end),
644            });
645        }
646
647        if function_name.eq_ignore_ascii_case("COUNT") {
648            if self.consume(&Token::Distinct)? {
649                let arg = self.parse_expr_prec(0)?;
650                self.expect(Token::RParen)?;
651                let end = self.position();
652                return Ok(Expr::FunctionCall {
653                    name: "COUNT_DISTINCT".to_string(),
654                    args: vec![arg],
655                    span: Span::new(start, end),
656                });
657            }
658
659            if self.consume(&Token::Star)? {
660                self.expect(Token::RParen)?;
661                let end = self.position();
662                return Ok(Expr::FunctionCall {
663                    name: function_name,
664                    args: vec![Expr::Column {
665                        field: FieldRef::TableColumn {
666                            table: String::new(),
667                            column: "*".to_string(),
668                        },
669                        span: Span::synthetic(),
670                    }],
671                    span: Span::new(start, end),
672                });
673            }
674        }
675
676        // CONFIG()/KV() take bare dotted config paths as arguments
677        // (e.g. `CONFIG(red.ai.default.provider, openai)`,
678        // `KV(cfg, default.role, guest)`). Parsed through the generic
679        // expression grammar these become column references — and a
680        // keyword segment like `default` would be folded to `DEFAULT`,
681        // breaking the case-sensitive config-key lookup, while a
682        // source-free `SELECT CONFIG(...)` would fail with "unknown
683        // column". Capture each path-shaped argument as a lowercased
684        // string literal instead so it matches stored keys (which
685        // `SET CONFIG` also lowercases) and never resolves as a column.
686        if function_name.eq_ignore_ascii_case("CONFIG") || function_name.eq_ignore_ascii_case("KV")
687        {
688            let mut args = Vec::new();
689            if !self.check(&Token::RParen) {
690                loop {
691                    args.push(self.parse_config_kv_arg(start)?);
692                    if !self.consume(&Token::Comma)? {
693                        break;
694                    }
695                }
696            }
697            self.expect(Token::RParen)?;
698            let end = self.position();
699            return Ok(Expr::FunctionCall {
700                name: function_name,
701                args,
702                span: Span::new(start, end),
703            });
704        }
705
706        let mut args = Vec::new();
707        if !self.check(&Token::RParen) {
708            loop {
709                args.push(self.parse_expr_prec(0)?);
710                if !self.consume(&Token::Comma)? {
711                    break;
712                }
713            }
714        }
715        self.expect(Token::RParen)?;
716        let end = self.position();
717        Ok(Expr::FunctionCall {
718            name: function_name,
719            args,
720            span: Span::new(start, end),
721        })
722    }
723
724    /// Parse the geo-column argument of `GEO_WITHIN`: one or more
725    /// dot-separated word-shaped segments, each captured in the spelling
726    /// the user typed. The leading segment becomes the qualifier so
727    /// `body.location.gps` resolves the same way it does for
728    /// `GEO_DISTANCE`.
729    fn parse_geo_column_expr(&mut self) -> Result<Expr, ParseError> {
730        let start = self.position();
731        let mut segments = vec![self.expect_spatial_column_segment()?];
732        while self.consume(&Token::Dot)? {
733            segments.push(self.expect_spatial_column_segment()?);
734        }
735        let table = if segments.len() > 1 {
736            segments.remove(0)
737        } else {
738            String::new()
739        };
740        Ok(Expr::Column {
741            field: FieldRef::TableColumn {
742                table,
743                column: segments.join("."),
744            },
745            span: Span::new(start, self.position()),
746        })
747    }
748
749    /// Parse `POLYGON`'s vertex list into a flat `[lat0, lon0, lat1, lon1, …]`
750    /// argument vector. When every coordinate is a numeric literal the
751    /// polygon is validated here against the same input rules the
752    /// `SEARCH SPATIAL WITHIN POLYGON` verb enforces; a polygon built
753    /// from columns or other runtime expressions is left to the executor.
754    fn parse_polygon_vertex_args(&mut self) -> Result<Vec<Expr>, ParseError> {
755        let pos = self.position();
756        let mut args = Vec::new();
757        loop {
758            self.expect(Token::LParen)?;
759            args.push(self.parse_polygon_coordinate()?);
760            args.push(self.parse_polygon_coordinate()?);
761            self.expect(Token::RParen)?;
762            if !self.consume(&Token::Comma)? {
763                break;
764            }
765        }
766        if let Some(vertices) = crate::ast::geo_predicate::literal_polygon_vertices(&args) {
767            super::search_commands::validate_polygon_vertices(&vertices, "POLYGON", pos)?;
768        } else if args.len() < 6 || !args.len().is_multiple_of(2) {
769            return Err(ParseError::new(
770                "POLYGON requires at least 3 vertices".to_string(),
771                pos,
772            ));
773        }
774        Ok(args)
775    }
776
777    /// One `POLYGON` coordinate.
778    ///
779    /// A number-shaped coordinate — with or without a leading sign — is
780    /// read as a single `f64` literal, exactly as the `SEARCH SPATIAL
781    /// WITHIN POLYGON` verb reads it. That matters beyond spelling: left
782    /// to the generic grammar, `-77.20` would become a negation *node*
783    /// wrapping a literal, and the planner's constant-polygon test would
784    /// reject every western-hemisphere polygon as non-constant.
785    ///
786    /// Anything else is a runtime expression (a column, a parameter). It
787    /// parses above the additive precedence so the space-separated
788    /// `(lat lon)` pair cannot collapse into one subtraction node.
789    fn parse_polygon_coordinate(&mut self) -> Result<Expr, ParseError> {
790        const ABOVE_ADDITIVE: u8 = 70;
791        let start = self.position();
792        if self.polygon_coordinate_is_numeric()? {
793            let value = self.parse_float()?;
794            return Ok(Expr::Literal {
795                value: Value::Float(value),
796                span: Span::new(start, self.position()),
797            });
798        }
799        self.parse_expr_prec(ABOVE_ADDITIVE)
800    }
801
802    fn polygon_coordinate_is_numeric(&mut self) -> Result<bool, ParseError> {
803        if matches!(self.peek(), Token::Float(_) | Token::Integer(_)) {
804            return Ok(true);
805        }
806        if matches!(self.peek(), Token::Minus | Token::Dash) {
807            return Ok(matches!(
808                self.peek_next()?,
809                Token::Float(_) | Token::Integer(_)
810            ));
811        }
812        Ok(false)
813    }
814
815    /// Parse a single CONFIG()/KV() argument. A bare identifier or
816    /// dotted path (including keyword-shaped segments) becomes a
817    /// lowercased string literal — the config-key form. Anything else
818    /// (quoted string, number, `?`/`$N` placeholder, parenthesised
819    /// expression) falls through to the normal expression grammar so
820    /// dynamic defaults still work.
821    fn parse_config_kv_arg(&mut self, start: crate::lexer::Position) -> Result<Expr, ParseError> {
822        // Literals, placeholders and parenthesised sub-expressions are
823        // real expressions (dynamic defaults); everything else that can
824        // open an argument here is an identifier or keyword that forms a
825        // bare config path.
826        let mut is_expression_start = matches!(
827            self.peek(),
828            Token::String(_)
829                | Token::Integer(_)
830                | Token::Float(_)
831                | Token::Dollar
832                | Token::Question
833                | Token::LParen
834        );
835        // A bare identifier immediately followed by `(` is a nested
836        // function call (e.g. a dynamic default), not a config path.
837        if matches!(self.peek(), Token::Ident(_)) && matches!(self.peek_next()?, Token::LParen) {
838            is_expression_start = true;
839        }
840        if !is_expression_start && !self.check(&Token::RParen) {
841            let mut path = self.expect_ident_or_keyword()?;
842            while self.consume(&Token::Dot)? {
843                let next = self.expect_ident_or_keyword()?;
844                path = format!("{path}.{next}");
845            }
846            let end = self.position();
847            return Ok(Expr::Literal {
848                value: Value::text(path.to_ascii_lowercase()),
849                span: Span::new(start, end),
850            });
851        }
852        self.parse_expr_prec(0)
853    }
854
855    /// Wrap a freshly-parsed `Expr::FunctionCall` in
856    /// `Expr::WindowFunctionCall` by consuming the trailing `OVER (...)`
857    /// clause. The caller has already confirmed the next token is
858    /// `OVER`. Rejects:
859    /// - CAST(...) OVER (...) and other non-FunctionCall shapes.
860    /// - Function names that are neither window-only nor aggregates.
861    fn lift_to_window_call(
862        &mut self,
863        start: crate::lexer::Position,
864        call: Expr,
865    ) -> Result<Expr, ParseError> {
866        let (name, args) = match call {
867            Expr::FunctionCall { name, args, .. } => (name, args),
868            other => {
869                return Err(ParseError::new(
870                    format!(
871                        "OVER may only follow a function call, got {:?}",
872                        std::mem::discriminant(&other)
873                    ),
874                    self.position(),
875                ));
876            }
877        };
878        if !is_window_eligible_function(&name) {
879            return Err(ParseError::new(
880                format!(
881                    "function `{}` cannot be used with an OVER clause; \
882                     expected a window function (LAG, LEAD, ROW_NUMBER, \
883                     RANK, DENSE_RANK) or an aggregate",
884                    name.to_uppercase()
885                ),
886                self.position(),
887            ));
888        }
889        let window = self.parse_over_clause()?;
890        let end = self.position();
891        Ok(Expr::WindowFunctionCall {
892            name,
893            args,
894            window,
895            span: Span::new(start, end),
896        })
897    }
898
899    /// Parse the `OVER ( [PARTITION BY ...] [ORDER BY ...] [frame] )`
900    /// clause. The leading `OVER` keyword is consumed here.
901    fn parse_over_clause(&mut self) -> Result<crate::ast::WindowSpec, ParseError> {
902        self.expect(Token::Over)?;
903        self.expect(Token::LParen)?;
904
905        let mut spec = crate::ast::WindowSpec::default();
906
907        if self.consume(&Token::Partition)? {
908            self.expect(Token::By)?;
909            loop {
910                spec.partition_by.push(self.parse_expr_prec(0)?);
911                if !self.consume(&Token::Comma)? {
912                    break;
913                }
914            }
915        }
916
917        if self.consume(&Token::Order)? {
918            self.expect(Token::By)?;
919            loop {
920                let expr = self.parse_expr_prec(0)?;
921                let ascending = if self.consume(&Token::Desc)? {
922                    false
923                } else {
924                    self.consume(&Token::Asc)?;
925                    true
926                };
927                // NULLS FIRST / LAST defaults mirror PG: nulls last for
928                // ASC, nulls first for DESC. Explicit clause overrides.
929                let mut nulls_first = !ascending;
930                if self.consume(&Token::Nulls)? {
931                    if self.consume(&Token::First)? {
932                        nulls_first = true;
933                    } else if self.consume(&Token::Last)? {
934                        nulls_first = false;
935                    } else {
936                        return Err(ParseError::new(
937                            "expected FIRST or LAST after NULLS".to_string(),
938                            self.position(),
939                        ));
940                    }
941                }
942                spec.order_by.push(crate::ast::WindowOrderItem {
943                    expr,
944                    ascending,
945                    nulls_first,
946                });
947                if !self.consume(&Token::Comma)? {
948                    break;
949                }
950            }
951        }
952
953        if matches!(self.peek(), Token::Rows | Token::Range) {
954            spec.frame = Some(self.parse_window_frame()?);
955        }
956
957        self.expect(Token::RParen)?;
958        Ok(spec)
959    }
960
961    fn parse_window_frame(&mut self) -> Result<crate::ast::WindowFrame, ParseError> {
962        let unit = if self.consume(&Token::Rows)? {
963            crate::ast::WindowFrameUnit::Rows
964        } else if self.consume(&Token::Range)? {
965            crate::ast::WindowFrameUnit::Range
966        } else {
967            return Err(ParseError::new(
968                "expected ROWS or RANGE in window frame".to_string(),
969                self.position(),
970            ));
971        };
972
973        if self.consume(&Token::Between)? {
974            let start = self.parse_window_frame_bound()?;
975            self.expect(Token::And)?;
976            let end = self.parse_window_frame_bound()?;
977            Ok(crate::ast::WindowFrame {
978                unit,
979                start,
980                end: Some(end),
981            })
982        } else {
983            let start = self.parse_window_frame_bound()?;
984            Ok(crate::ast::WindowFrame {
985                unit,
986                start,
987                end: None,
988            })
989        }
990    }
991
992    fn parse_window_frame_bound(&mut self) -> Result<crate::ast::WindowFrameBound, ParseError> {
993        use crate::ast::WindowFrameBound;
994        if self.consume(&Token::Unbounded)? {
995            if self.consume(&Token::Preceding)? {
996                return Ok(WindowFrameBound::UnboundedPreceding);
997            }
998            if self.consume(&Token::Following)? {
999                return Ok(WindowFrameBound::UnboundedFollowing);
1000            }
1001            return Err(ParseError::new(
1002                "expected PRECEDING or FOLLOWING after UNBOUNDED".to_string(),
1003                self.position(),
1004            ));
1005        }
1006        if self.consume(&Token::Current)? {
1007            self.expect(Token::Row)?;
1008            return Ok(WindowFrameBound::CurrentRow);
1009        }
1010        // Numeric / expression offset: `N PRECEDING` / `N FOLLOWING`.
1011        let offset = self.parse_expr_prec(0)?;
1012        if self.consume(&Token::Preceding)? {
1013            return Ok(WindowFrameBound::Preceding(Box::new(offset)));
1014        }
1015        if self.consume(&Token::Following)? {
1016            return Ok(WindowFrameBound::Following(Box::new(offset)));
1017        }
1018        Err(ParseError::new(
1019            "expected PRECEDING or FOLLOWING after frame offset".to_string(),
1020            self.position(),
1021        ))
1022    }
1023
1024    /// Parse both CASE forms:
1025    /// - searched: `CASE WHEN cond THEN val [WHEN …] [ELSE val] END`
1026    /// - simple:   `CASE expr WHEN val THEN val [WHEN …] [ELSE val] END`
1027    ///
1028    /// The simple form is desugared into the searched form: each
1029    /// `WHEN <value>` becomes the equality condition `<selector> = <value>`,
1030    /// which preserves SQL's three-valued comparison semantics (a NULL
1031    /// selector never matches a WHEN value) without growing the `Expr::Case`
1032    /// AST or the executor.
1033    ///
1034    /// Assumes the caller has already peeked `CASE`.
1035    fn parse_case_expr(&mut self, start: crate::lexer::Position) -> Result<Expr, ParseError> {
1036        self.advance()?; // consume CASE
1037                         // Simple CASE: a selector expression precedes the first WHEN.
1038        let selector = if matches!(self.peek(), Token::Ident(id) if id.eq_ignore_ascii_case("WHEN"))
1039        {
1040            None
1041        } else {
1042            Some(self.parse_expr_prec(0)?)
1043        };
1044        let mut branches: Vec<(Expr, Expr)> = Vec::new();
1045        loop {
1046            if !self.consume_ident_ci("WHEN")? {
1047                break;
1048            }
1049            let when_val = self.parse_expr_prec(0)?;
1050            // Searched form keeps the WHEN expression as the condition;
1051            // simple form rewrites it to `selector = when_val`.
1052            let cond = match &selector {
1053                None => when_val,
1054                Some(sel) => {
1055                    let span = Span::new(sel.span().start, when_val.span().end);
1056                    Expr::BinaryOp {
1057                        op: BinOp::Eq,
1058                        lhs: Box::new(sel.clone()),
1059                        rhs: Box::new(when_val),
1060                        span,
1061                    }
1062                }
1063            };
1064            if !self.consume_ident_ci("THEN")? {
1065                return Err(ParseError::new(
1066                    "expected THEN after CASE WHEN condition".to_string(),
1067                    self.position(),
1068                ));
1069            }
1070            let then_val = self.parse_expr_prec(0)?;
1071            branches.push((cond, then_val));
1072        }
1073        if branches.is_empty() {
1074            return Err(ParseError::new(
1075                "CASE must have at least one WHEN branch".to_string(),
1076                self.position(),
1077            ));
1078        }
1079        let else_ = if self.consume_ident_ci("ELSE")? {
1080            Some(Box::new(self.parse_expr_prec(0)?))
1081        } else {
1082            None
1083        };
1084        if !self.consume_ident_ci("END")? {
1085            return Err(ParseError::new(
1086                "expected END to close CASE expression".to_string(),
1087                self.position(),
1088            ));
1089        }
1090        let end = self.position();
1091        Ok(Expr::Case {
1092            branches,
1093            else_,
1094            span: Span::new(start, end),
1095        })
1096    }
1097
1098    fn parse_trim_expr_args(&mut self) -> Result<(String, Vec<Expr>), ParseError> {
1099        let mut function_name = "TRIM".to_string();
1100
1101        if self.consume_ident_ci("LEADING")? {
1102            function_name = "LTRIM".to_string();
1103        } else if self.consume_ident_ci("TRAILING")? {
1104            function_name = "RTRIM".to_string();
1105        } else if self.consume_ident_ci("BOTH")? {
1106            function_name = "TRIM".to_string();
1107        }
1108
1109        if self.consume(&Token::From)? {
1110            let source = self.parse_expr_prec(0)?;
1111            return Ok((function_name, vec![source]));
1112        }
1113
1114        let first = self.parse_expr_prec(0)?;
1115
1116        if self.consume(&Token::Comma)? {
1117            let second = self.parse_expr_prec(0)?;
1118            return Ok((function_name, vec![first, second]));
1119        }
1120
1121        if self.consume(&Token::From)? {
1122            let source = self.parse_expr_prec(0)?;
1123            return Ok((function_name, vec![source, first]));
1124        }
1125
1126        Ok((function_name, vec![first]))
1127    }
1128
1129    /// PostgreSQL-style `POSITION(substr IN string)` or plain
1130    /// `POSITION(substr, string)` lowered to the ordinary two-argument
1131    /// function form.
1132    fn parse_position_expr_args(&mut self) -> Result<Vec<Expr>, ParseError> {
1133        // `IN` is also a postfix operator in the main expression grammar, so
1134        // parse the first operand above postfix-IN precedence and then consume
1135        // the function's `IN` keyword explicitly.
1136        let needle = self.parse_expr_prec(35)?;
1137        if !self.consume(&Token::Comma)? {
1138            self.expect(Token::In)?;
1139        }
1140        let haystack = self.parse_expr_prec(0)?;
1141        Ok(vec![needle, haystack])
1142    }
1143
1144    /// PostgreSQL-style `SUBSTRING` syntax:
1145    /// - `SUBSTRING(expr FROM start [FOR count])`
1146    /// - `SUBSTRING(expr FOR count [FROM start])`
1147    /// - plain function-call form `SUBSTRING(expr, start[, count])`
1148    ///
1149    /// The SQL-syntax variants are desugared to the comma-arg form so the
1150    /// rest of the stack sees the same `Expr::FunctionCall` shape.
1151    fn parse_substring_expr_args(&mut self) -> Result<Vec<Expr>, ParseError> {
1152        let source = self.parse_expr_prec(0)?;
1153
1154        if self.consume(&Token::Comma)? {
1155            let mut args = vec![source];
1156            loop {
1157                args.push(self.parse_expr_prec(0)?);
1158                if !self.consume(&Token::Comma)? {
1159                    break;
1160                }
1161            }
1162            return Ok(args);
1163        }
1164
1165        if self.consume(&Token::From)? {
1166            let start = self.parse_expr_prec(0)?;
1167            if self.consume(&Token::For)? {
1168                let count = self.parse_expr_prec(0)?;
1169                return Ok(vec![source, start, count]);
1170            }
1171            return Ok(vec![source, start]);
1172        }
1173
1174        if self.consume(&Token::For)? {
1175            let count = self.parse_expr_prec(0)?;
1176            if self.consume(&Token::From)? {
1177                let start = self.parse_expr_prec(0)?;
1178                return Ok(vec![source, start, count]);
1179            }
1180            return Ok(vec![source, Expr::lit(Value::Integer(1)), count]);
1181        }
1182
1183        Ok(vec![source])
1184    }
1185
1186    /// Try to consume a postfix operator on top of the already-parsed
1187    /// `left` expression: `IS [NOT] NULL`, `[NOT] BETWEEN … AND …`,
1188    /// `[NOT] IN (…)`. Returns `Ok(None)` if no postfix follows.
1189    ///
1190    /// NOT at this position is unambiguous — prefix `NOT` is always
1191    /// consumed at `parse_expr_unary` level before reaching postfix.
1192    /// So seeing `NOT` here means the user wrote `x NOT BETWEEN …`
1193    /// or `x NOT IN …`; we consume it eagerly and require BETWEEN
1194    /// or IN to follow.
1195    fn try_parse_postfix(&mut self, left: &Expr) -> Result<Option<Expr>, ParseError> {
1196        let start = self.span_start_of(left);
1197
1198        // IS [NOT] NULL
1199        if self.consume(&Token::Is)? {
1200            let negated = self.consume(&Token::Not)?;
1201            self.expect(Token::Null)?;
1202            let end = self.position();
1203            return Ok(Some(Expr::IsNull {
1204                operand: Box::new(left.clone()),
1205                negated,
1206                span: Span::new(start, end),
1207            }));
1208        }
1209
1210        // Detect NOT BETWEEN / NOT IN. NOT is consumed eagerly — we
1211        // don't have two-token lookahead and the grammar guarantees
1212        // no other valid postfix starts with NOT.
1213        let negated = if matches!(self.peek(), Token::Not) {
1214            self.advance()?;
1215            if !matches!(self.peek(), Token::Between | Token::In) {
1216                return Err(ParseError::new(
1217                    "expected BETWEEN or IN after postfix NOT".to_string(),
1218                    self.position(),
1219                ));
1220            }
1221            true
1222        } else {
1223            false
1224        };
1225
1226        // BETWEEN low AND high
1227        if self.consume(&Token::Between)? {
1228            let low = self.parse_expr_prec(34)?;
1229            self.expect(Token::And)?;
1230            let high = self.parse_expr_prec(34)?;
1231            let end = self.position();
1232            return Ok(Some(Expr::Between {
1233                target: Box::new(left.clone()),
1234                low: Box::new(low),
1235                high: Box::new(high),
1236                negated,
1237                span: Span::new(start, end),
1238            }));
1239        }
1240
1241        // IN (v1, v2, …)
1242        if self.consume(&Token::In)? {
1243            self.expect(Token::LParen)?;
1244            let mut values = Vec::new();
1245            if self.check(&Token::Select) {
1246                let query = self.parse_select_query()?;
1247                values.push(Expr::Subquery {
1248                    query: ExprSubquery {
1249                        query: Box::new(query),
1250                    },
1251                    span: Span::new(self.span_start_of(left), self.position()),
1252                });
1253            } else if !self.check(&Token::RParen) {
1254                loop {
1255                    values.push(self.parse_expr_prec(0)?);
1256                    if !self.consume(&Token::Comma)? {
1257                        break;
1258                    }
1259                }
1260            }
1261            self.expect(Token::RParen)?;
1262            let end = self.position();
1263            return Ok(Some(Expr::InList {
1264                target: Box::new(left.clone()),
1265                values,
1266                negated,
1267                span: Span::new(start, end),
1268            }));
1269        }
1270
1271        if negated {
1272            // Unreachable because the early-return above already
1273            // validated NOT is followed by BETWEEN or IN. Guarded
1274            // to keep callers loud if the grammar grows later.
1275            return Err(ParseError::new(
1276                "internal: NOT consumed without BETWEEN/IN follow".to_string(),
1277                self.position(),
1278            ));
1279        }
1280        Ok(None)
1281    }
1282
1283    /// Peek the current token and translate it into a `BinOp` plus
1284    /// its precedence. Returns `None` if the token is not a recognised
1285    /// infix operator — the caller then tries postfix handling.
1286    fn peek_binop(&self) -> Option<(BinOp, u8)> {
1287        let op = match self.peek() {
1288            Token::Or => BinOp::Or,
1289            Token::And => BinOp::And,
1290            Token::Eq => BinOp::Eq,
1291            Token::Ne => BinOp::Ne,
1292            Token::Lt => BinOp::Lt,
1293            Token::Le => BinOp::Le,
1294            Token::Gt => BinOp::Gt,
1295            Token::Ge => BinOp::Ge,
1296            Token::DoublePipe => BinOp::Concat,
1297            Token::Plus => BinOp::Add,
1298            Token::Dash => BinOp::Sub,
1299            Token::Star => BinOp::Mul,
1300            Token::Slash => BinOp::Div,
1301            Token::Percent => BinOp::Mod,
1302            _ => return None,
1303        };
1304        Some((op, op.precedence()))
1305    }
1306
1307    /// Return the start position of an expression's span. Handles the
1308    /// synthetic case by falling back to the current parser cursor,
1309    /// which is good enough for the Pratt climb since the caller just
1310    /// parsed the atom.
1311    fn span_start_of(&self, expr: &Expr) -> crate::lexer::Position {
1312        let s = expr.span();
1313        if s.is_synthetic() {
1314            self.position()
1315        } else {
1316            s.start
1317        }
1318    }
1319
1320    /// Return the end position of an expression's span — same
1321    /// synthetic fallback as `span_start_of`.
1322    fn span_end_of(&self, expr: &Expr) -> crate::lexer::Position {
1323        let s = expr.span();
1324        if s.is_synthetic() {
1325            self.position()
1326        } else {
1327            s.end
1328        }
1329    }
1330}
1331
1332// Avoid `unused` lints in partial-migration builds where the analyzer
1333// still does not consume every expression shape directly.
1334#[allow(dead_code)]
1335fn _expr_module_used(_: Expr) {}
1336
1337#[cfg(test)]
1338mod tests {
1339    use super::*;
1340    use crate::ast::FieldRef;
1341
1342    fn parse(input: &str) -> Expr {
1343        let mut parser = Parser::new(input).expect("lexer init");
1344        let expr = parser.parse_expr().expect("parse_expr");
1345        expr
1346    }
1347
1348    #[test]
1349    fn literal_integer() {
1350        let e = parse("42");
1351        match e {
1352            Expr::Literal {
1353                value: Value::Integer(42),
1354                ..
1355            } => {}
1356            other => panic!("expected Integer(42), got {other:?}"),
1357        }
1358    }
1359
1360    #[test]
1361    fn literal_float() {
1362        let e = parse("3.14");
1363        match e {
1364            Expr::Literal {
1365                value: Value::Float(f),
1366                ..
1367            } => assert!((f - 3.14).abs() < 1e-9),
1368            other => panic!("expected float literal, got {other:?}"),
1369        }
1370    }
1371
1372    #[test]
1373    fn literal_string() {
1374        let e = parse("'hello'");
1375        match e {
1376            Expr::Literal {
1377                value: Value::Text(ref s),
1378                ..
1379            } if s.as_ref() == "hello" => {}
1380            other => panic!("expected Text(hello), got {other:?}"),
1381        }
1382    }
1383
1384    #[test]
1385    fn literal_booleans_and_null() {
1386        assert!(matches!(
1387            parse("TRUE"),
1388            Expr::Literal {
1389                value: Value::Boolean(true),
1390                ..
1391            }
1392        ));
1393        assert!(matches!(
1394            parse("FALSE"),
1395            Expr::Literal {
1396                value: Value::Boolean(false),
1397                ..
1398            }
1399        ));
1400        assert!(matches!(
1401            parse("NULL"),
1402            Expr::Literal {
1403                value: Value::Null,
1404                ..
1405            }
1406        ));
1407    }
1408
1409    #[test]
1410    fn bare_column() {
1411        let e = parse("user_id");
1412        match e {
1413            Expr::Column {
1414                field: FieldRef::TableColumn { column, .. },
1415                ..
1416            } => {
1417                assert_eq!(column, "user_id");
1418            }
1419            other => panic!("expected column, got {other:?}"),
1420        }
1421    }
1422
1423    #[test]
1424    fn arithmetic_precedence_mul_over_add() {
1425        // a + b * c  →  Add(a, Mul(b, c))
1426        let e = parse("a + b * c");
1427        let Expr::BinaryOp {
1428            op: BinOp::Add,
1429            rhs,
1430            ..
1431        } = e
1432        else {
1433            panic!("root must be Add");
1434        };
1435        let Expr::BinaryOp { op: BinOp::Mul, .. } = *rhs else {
1436            panic!("rhs must be Mul");
1437        };
1438    }
1439
1440    #[test]
1441    fn arithmetic_left_associativity() {
1442        // a - b - c  →  Sub(Sub(a, b), c)
1443        let e = parse("a - b - c");
1444        let Expr::BinaryOp {
1445            op: BinOp::Sub,
1446            lhs,
1447            ..
1448        } = e
1449        else {
1450            panic!("root must be Sub");
1451        };
1452        let Expr::BinaryOp { op: BinOp::Sub, .. } = *lhs else {
1453            panic!("lhs must be Sub (left-assoc)");
1454        };
1455    }
1456
1457    #[test]
1458    fn parenthesised_override() {
1459        // (a + b) * c  →  Mul(Add(a, b), c)
1460        let e = parse("(a + b) * c");
1461        let Expr::BinaryOp {
1462            op: BinOp::Mul,
1463            lhs,
1464            ..
1465        } = e
1466        else {
1467            panic!("root must be Mul");
1468        };
1469        let Expr::BinaryOp { op: BinOp::Add, .. } = *lhs else {
1470            panic!("lhs must be Add");
1471        };
1472    }
1473
1474    #[test]
1475    fn comparison_binds_weaker_than_arith() {
1476        // a + 1 = b - 2
1477        //   →  Eq(Add(a, 1), Sub(b, 2))
1478        let e = parse("a + 1 = b - 2");
1479        let Expr::BinaryOp {
1480            op: BinOp::Eq,
1481            lhs,
1482            rhs,
1483            ..
1484        } = e
1485        else {
1486            panic!("root must be Eq");
1487        };
1488        assert!(matches!(*lhs, Expr::BinaryOp { op: BinOp::Add, .. }));
1489        assert!(matches!(*rhs, Expr::BinaryOp { op: BinOp::Sub, .. }));
1490    }
1491
1492    #[test]
1493    fn and_binds_tighter_than_or() {
1494        // a OR b AND c  →  Or(a, And(b, c))
1495        let e = parse("a OR b AND c");
1496        let Expr::BinaryOp {
1497            op: BinOp::Or, rhs, ..
1498        } = e
1499        else {
1500            panic!("root must be Or");
1501        };
1502        assert!(matches!(*rhs, Expr::BinaryOp { op: BinOp::And, .. }));
1503    }
1504
1505    #[test]
1506    fn unary_negation() {
1507        let e = parse("-a");
1508        let Expr::UnaryOp {
1509            op: UnaryOp::Neg, ..
1510        } = e
1511        else {
1512            panic!("expected unary Neg");
1513        };
1514    }
1515
1516    #[test]
1517    fn unary_not() {
1518        let e = parse("NOT a");
1519        let Expr::UnaryOp {
1520            op: UnaryOp::Not, ..
1521        } = e
1522        else {
1523            panic!("expected unary Not");
1524        };
1525    }
1526
1527    #[test]
1528    fn concat_operator() {
1529        let e = parse("'hello' || name");
1530        let Expr::BinaryOp {
1531            op: BinOp::Concat, ..
1532        } = e
1533        else {
1534            panic!("expected Concat");
1535        };
1536    }
1537
1538    #[test]
1539    fn cast_expr() {
1540        let e = parse("CAST(age AS TEXT)");
1541        let Expr::Cast { target, .. } = e else {
1542            panic!("expected Cast");
1543        };
1544        assert_eq!(target, DataType::Text);
1545    }
1546
1547    #[test]
1548    fn case_expr() {
1549        let e = parse("CASE WHEN a = 1 THEN 'one' WHEN a = 2 THEN 'two' ELSE 'other' END");
1550        let Expr::Case {
1551            branches, else_, ..
1552        } = e
1553        else {
1554            panic!("expected Case");
1555        };
1556        assert_eq!(branches.len(), 2);
1557        assert!(else_.is_some());
1558    }
1559
1560    #[test]
1561    fn simple_case_desugars_to_equality() {
1562        let e = parse("CASE id WHEN 1 THEN 'one' WHEN 2 THEN 'two' ELSE 'many' END");
1563        let Expr::Case {
1564            branches, else_, ..
1565        } = e
1566        else {
1567            panic!("expected Case");
1568        };
1569        assert_eq!(branches.len(), 2);
1570        assert!(else_.is_some());
1571        // Each WHEN value is rewritten to `selector = value`.
1572        for (cond, _) in &branches {
1573            let Expr::BinaryOp { op, lhs, .. } = cond else {
1574                panic!("expected desugared equality condition");
1575            };
1576            assert_eq!(*op, BinOp::Eq);
1577            assert!(matches!(**lhs, Expr::Column { .. }));
1578        }
1579    }
1580
1581    #[test]
1582    fn is_null_postfix() {
1583        let e = parse("name IS NULL");
1584        assert!(matches!(e, Expr::IsNull { negated: false, .. }));
1585    }
1586
1587    #[test]
1588    fn is_not_null_postfix() {
1589        let e = parse("name IS NOT NULL");
1590        assert!(matches!(e, Expr::IsNull { negated: true, .. }));
1591    }
1592
1593    #[test]
1594    fn between_with_columns() {
1595        let e = parse("temp BETWEEN min_t AND max_t");
1596        let Expr::Between {
1597            target,
1598            low,
1599            high,
1600            negated,
1601            ..
1602        } = e
1603        else {
1604            panic!("expected Between");
1605        };
1606        assert!(!negated);
1607        assert!(matches!(*target, Expr::Column { .. }));
1608        assert!(matches!(*low, Expr::Column { .. }));
1609        assert!(matches!(*high, Expr::Column { .. }));
1610    }
1611
1612    #[test]
1613    fn not_between_negates() {
1614        let e = parse("temp NOT BETWEEN 0 AND 100");
1615        let Expr::Between { negated: true, .. } = e else {
1616            panic!("expected negated Between");
1617        };
1618    }
1619
1620    #[test]
1621    fn in_list_literal() {
1622        let e = parse("status IN (1, 2, 3)");
1623        let Expr::InList {
1624            values, negated, ..
1625        } = e
1626        else {
1627            panic!("expected InList");
1628        };
1629        assert!(!negated);
1630        assert_eq!(values.len(), 3);
1631    }
1632
1633    #[test]
1634    fn not_in_list() {
1635        let e = parse("status NOT IN (1, 2)");
1636        let Expr::InList { negated: true, .. } = e else {
1637            panic!("expected negated InList");
1638        };
1639    }
1640
1641    #[test]
1642    fn function_call_with_args() {
1643        let e = parse("UPPER(name)");
1644        let Expr::FunctionCall { name, args, .. } = e else {
1645            panic!("expected FunctionCall");
1646        };
1647        assert_eq!(name, "UPPER");
1648        assert_eq!(args.len(), 1);
1649    }
1650
1651    #[test]
1652    fn nested_function_call() {
1653        let e = parse("COALESCE(a, UPPER(b))");
1654        let Expr::FunctionCall { name, args, .. } = e else {
1655            panic!("expected FunctionCall");
1656        };
1657        assert_eq!(name, "COALESCE");
1658        assert_eq!(args.len(), 2);
1659        assert!(matches!(&args[1], Expr::FunctionCall { .. }));
1660    }
1661
1662    #[test]
1663    fn duration_literal_parses_as_text() {
1664        let e = parse("time_bucket(5m)");
1665        let Expr::FunctionCall { name, args, .. } = e else {
1666            panic!("expected FunctionCall, got {e:?}");
1667        };
1668        assert_eq!(name.to_uppercase(), "TIME_BUCKET");
1669        assert_eq!(args.len(), 1);
1670        assert!(
1671            matches!(&args[0], Expr::Literal { value: Value::Text(s), .. } if s.as_ref() == "5m"),
1672            "expected Text(\"5m\"), got {:?}",
1673            args[0]
1674        );
1675    }
1676
1677    #[test]
1678    fn placeholder_dollar_one() {
1679        let e = parse("$1");
1680        match e {
1681            Expr::Parameter { index: 0, .. } => {}
1682            other => panic!("expected Parameter(0), got {other:?}"),
1683        }
1684    }
1685
1686    #[test]
1687    fn placeholder_dollar_n() {
1688        let e = parse("$7");
1689        match e {
1690            Expr::Parameter { index: 6, .. } => {}
1691            other => panic!("expected Parameter(6), got {other:?}"),
1692        }
1693    }
1694
1695    #[test]
1696    fn placeholder_in_string_literal_is_text() {
1697        // `$1` inside a string literal must NOT parse as a placeholder.
1698        let e = parse("'$1'");
1699        match e {
1700            Expr::Literal {
1701                value: Value::Text(s),
1702                ..
1703            } if s.as_ref() == "$1" => {}
1704            other => panic!("expected text literal '$1', got {other:?}"),
1705        }
1706    }
1707
1708    #[test]
1709    fn placeholder_in_comparison() {
1710        // SELECT-WHERE shape: `id = $1`
1711        let e = parse("id = $1");
1712        let Expr::BinaryOp {
1713            op: BinOp::Eq, rhs, ..
1714        } = e
1715        else {
1716            panic!("root must be Eq");
1717        };
1718        assert!(matches!(*rhs, Expr::Parameter { index: 0, .. }));
1719    }
1720
1721    #[test]
1722    fn placeholder_zero_rejected() {
1723        let mut parser = Parser::new("$0").expect("lexer");
1724        let err = parser.parse_expr().unwrap_err();
1725        assert!(err.to_string().contains("placeholder"));
1726    }
1727
1728    #[test]
1729    fn placeholder_question_single() {
1730        // Lone `?` numbered as parameter 1 (index 0).
1731        let e = parse("?");
1732        match e {
1733            Expr::Parameter { index: 0, .. } => {}
1734            other => panic!("expected Parameter(0), got {other:?}"),
1735        }
1736    }
1737
1738    #[test]
1739    fn placeholder_question_numbered() {
1740        let e = parse("?7");
1741        match e {
1742            Expr::Parameter { index: 6, .. } => {}
1743            other => panic!("expected Parameter(6), got {other:?}"),
1744        }
1745    }
1746
1747    #[test]
1748    fn placeholder_question_numbered_zero_rejected() {
1749        let mut parser = Parser::new("?0").expect("lexer");
1750        let err = parser.parse_expr().unwrap_err();
1751        assert!(err.to_string().contains("placeholder"));
1752    }
1753
1754    #[test]
1755    fn placeholder_question_left_to_right() {
1756        // `id = ? AND name = ?` → params 0 and 1
1757        let e = parse("id = ? AND name = ?");
1758        let Expr::BinaryOp {
1759            op: BinOp::And,
1760            lhs,
1761            rhs,
1762            ..
1763        } = e
1764        else {
1765            panic!("root must be And");
1766        };
1767        let Expr::BinaryOp {
1768            op: BinOp::Eq,
1769            rhs: r1,
1770            ..
1771        } = *lhs
1772        else {
1773            panic!("lhs must be Eq");
1774        };
1775        assert!(matches!(*r1, Expr::Parameter { index: 0, .. }));
1776        let Expr::BinaryOp {
1777            op: BinOp::Eq,
1778            rhs: r2,
1779            ..
1780        } = *rhs
1781        else {
1782            panic!("rhs must be Eq");
1783        };
1784        assert!(matches!(*r2, Expr::Parameter { index: 1, .. }));
1785    }
1786
1787    #[test]
1788    fn placeholder_question_in_string_literal_is_text() {
1789        let e = parse("'?'");
1790        match e {
1791            Expr::Literal {
1792                value: Value::Text(s),
1793                ..
1794            } if s.as_ref() == "?" => {}
1795            other => panic!("expected text literal '?', got {other:?}"),
1796        }
1797    }
1798
1799    #[test]
1800    fn placeholder_mixing_question_then_dollar_rejected() {
1801        let mut parser = Parser::new("id = ? AND x = $2").expect("lexer");
1802        let err = parser.parse_expr().err().expect("should fail");
1803        assert!(
1804            err.to_string().contains("mix"),
1805            "expected mixing error, got: {err}"
1806        );
1807    }
1808
1809    #[test]
1810    fn placeholder_mixing_dollar_then_question_rejected() {
1811        let mut parser = Parser::new("id = $1 AND x = ?").expect("lexer");
1812        let err = parser.parse_expr().err().expect("should fail");
1813        assert!(
1814            err.to_string().contains("mix"),
1815            "expected mixing error, got: {err}"
1816        );
1817    }
1818
1819    #[test]
1820    fn placeholder_question_in_comment_ignored() {
1821        // `?` inside an SQL line comment must not bump the counter.
1822        // The expression after the comment is the only param.
1823        let mut parser = Parser::new("-- ? ignored\n  ?").expect("lexer");
1824        let e = parser.parse_expr().expect("parse_expr");
1825        match e {
1826            Expr::Parameter { index: 0, .. } => {}
1827            other => panic!("expected Parameter(0), got {other:?}"),
1828        }
1829    }
1830
1831    #[test]
1832    fn unary_plus_is_noop() {
1833        let e = parse("+42");
1834        assert!(matches!(
1835            e,
1836            Expr::Literal {
1837                value: Value::Integer(42),
1838                ..
1839            }
1840        ));
1841    }
1842
1843    #[test]
1844    fn parenthesised_select_becomes_subquery_expr() {
1845        let e = parse("(SELECT 1)");
1846        assert!(matches!(e, Expr::Subquery { .. }));
1847    }
1848
1849    #[test]
1850    fn bare_zero_arg_current_functions_parse_as_calls() {
1851        for (input, expected) in [
1852            ("CURRENT_TIMESTAMP", "CURRENT_TIMESTAMP"),
1853            ("CURRENT_DATE", "CURRENT_DATE"),
1854            ("CURRENT_TIME", "CURRENT_TIME"),
1855        ] {
1856            let e = parse(input);
1857            let Expr::FunctionCall { name, args, .. } = e else {
1858                panic!("expected FunctionCall for {input}");
1859            };
1860            assert_eq!(name, expected);
1861            assert!(args.is_empty());
1862        }
1863    }
1864
1865    #[test]
1866    fn keyword_function_names_parse_as_calls() {
1867        for (input, expected_len) in [
1868            ("COUNT(*)", 1),
1869            ("SUM(amount)", 1),
1870            ("LEFT(name, 2)", 2),
1871            ("RIGHT(name, 2)", 2),
1872            ("CONTAINS(body, 'red')", 2),
1873            ("KV(cfg, path)", 2),
1874        ] {
1875            let e = parse(input);
1876            let Expr::FunctionCall { args, .. } = e else {
1877                panic!("expected FunctionCall for {input}");
1878            };
1879            assert_eq!(args.len(), expected_len, "{input}");
1880        }
1881    }
1882
1883    #[test]
1884    fn count_distinct_lowers_to_count_distinct_function() {
1885        let e = parse("COUNT(DISTINCT user_id)");
1886        let Expr::FunctionCall { name, args, .. } = e else {
1887            panic!("expected FunctionCall");
1888        };
1889        assert_eq!(name, "COUNT_DISTINCT");
1890        assert_eq!(args.len(), 1);
1891    }
1892
1893    #[test]
1894    fn dollar_secret_and_config_refs_become_function_calls() {
1895        for (input, expected_name, expected_key) in [
1896            ("$secret.api_key", "__SECRET_REF", "red.vault/api_key"),
1897            ("$red.secret.api_key", "__SECRET_REF", "red.vault/api_key"),
1898            ("$red.secrets.api_key", "__SECRET_REF", "red.vault/api_key"),
1899            ("$config.ai.provider", "CONFIG", "red.config/ai.provider"),
1900            (
1901                "$red.config.ai.provider",
1902                "CONFIG",
1903                "red.config/ai.provider",
1904            ),
1905        ] {
1906            let e = parse(input);
1907            let Expr::FunctionCall { name, args, .. } = e else {
1908                panic!("expected FunctionCall for {input}");
1909            };
1910            assert_eq!(name, expected_name);
1911            assert!(matches!(
1912                &args[..],
1913                [Expr::Literal { value: Value::Text(key), .. }] if key.as_ref() == expected_key
1914            ));
1915        }
1916    }
1917
1918    #[test]
1919    fn dollar_ref_rejects_unknown_namespace() {
1920        let mut parser = Parser::new("$tenant.id").expect("lexer");
1921        let err = parser
1922            .parse_expr()
1923            .expect_err("unknown namespace should fail");
1924        assert!(err.to_string().contains("unknown $ reference"));
1925    }
1926
1927    #[test]
1928    fn config_and_kv_bare_path_args_lowercase_to_text() {
1929        let e = parse("CONFIG(Red.AI.Default.Provider, 'openai')");
1930        let Expr::FunctionCall { name, args, .. } = e else {
1931            panic!("expected FunctionCall");
1932        };
1933        assert_eq!(name, "CONFIG");
1934        assert_eq!(args.len(), 2);
1935        assert!(matches!(
1936            &args[0],
1937            Expr::Literal { value: Value::Text(path), .. }
1938                if path.as_ref() == "red.ai.default.provider"
1939        ));
1940        assert!(matches!(
1941            &args[1],
1942            Expr::Literal { value: Value::Text(provider), .. } if provider.as_ref() == "openai"
1943        ));
1944
1945        let e = parse("KV(cfg, default.role, LOWER(name))");
1946        let Expr::FunctionCall { name, args, .. } = e else {
1947            panic!("expected FunctionCall");
1948        };
1949        assert_eq!(name, "KV");
1950        assert!(matches!(
1951            &args[0],
1952            Expr::Literal { value: Value::Text(path), .. } if path.as_ref() == "cfg"
1953        ));
1954        assert!(matches!(
1955            &args[1],
1956            Expr::Literal { value: Value::Text(path), .. } if path.as_ref() == "default.role"
1957        ));
1958        assert!(matches!(&args[2], Expr::FunctionCall { name, .. } if name == "LOWER"));
1959    }
1960
1961    #[test]
1962    fn cast_rejects_unknown_type_name() {
1963        let mut parser = Parser::new("CAST(age AS BOGUS_TYPE)").expect("lexer");
1964        let err = parser
1965            .parse_expr()
1966            .expect_err("unknown cast target should fail");
1967        assert!(err.to_string().contains("unknown type name"));
1968    }
1969
1970    #[test]
1971    fn trim_position_and_substring_sql_forms_lower_to_function_args() {
1972        let e = parse("TRIM(LEADING 'x' FROM name)");
1973        let Expr::FunctionCall { name, args, .. } = e else {
1974            panic!("expected trim function");
1975        };
1976        assert_eq!(name, "LTRIM");
1977        assert_eq!(args.len(), 2);
1978
1979        let e = parse("TRIM(TRAILING FROM name)");
1980        let Expr::FunctionCall { name, args, .. } = e else {
1981            panic!("expected trim function");
1982        };
1983        assert_eq!(name, "RTRIM");
1984        assert_eq!(args.len(), 1);
1985
1986        let e = parse("POSITION('x' IN name)");
1987        let Expr::FunctionCall { name, args, .. } = e else {
1988            panic!("expected position function");
1989        };
1990        assert_eq!(name, "POSITION");
1991        assert_eq!(args.len(), 2);
1992
1993        let e = parse("POSITION('x', name)");
1994        let Expr::FunctionCall { args, .. } = e else {
1995            panic!("expected position function");
1996        };
1997        assert_eq!(args.len(), 2);
1998
1999        let e = parse("SUBSTRING(name FROM 2 FOR 3)");
2000        let Expr::FunctionCall { name, args, .. } = e else {
2001            panic!("expected substring function");
2002        };
2003        assert_eq!(name, "SUBSTRING");
2004        assert_eq!(args.len(), 3);
2005
2006        let e = parse("SUBSTRING(name FOR 3)");
2007        let Expr::FunctionCall { args, .. } = e else {
2008            panic!("expected substring function");
2009        };
2010        assert_eq!(args.len(), 3);
2011        assert!(matches!(
2012            args[1],
2013            Expr::Literal {
2014                value: Value::Integer(1),
2015                ..
2016            }
2017        ));
2018    }
2019
2020    #[test]
2021    fn postfix_in_accepts_subquery_and_empty_list() {
2022        let e = parse("id IN (SELECT user_id FROM users)");
2023        let Expr::InList { values, .. } = e else {
2024            panic!("expected InList");
2025        };
2026        assert!(matches!(&values[..], [Expr::Subquery { .. }]));
2027
2028        let e = parse("id IN ()");
2029        let Expr::InList { values, .. } = e else {
2030            panic!("expected InList");
2031        };
2032        assert!(values.is_empty());
2033    }
2034
2035    #[test]
2036    fn postfix_not_requires_between_or_in() {
2037        let mut parser = Parser::new("status NOT NULL").expect("lexer");
2038        let err = parser.parse_expr().expect_err("postfix NOT should fail");
2039        assert!(err.to_string().contains("BETWEEN or IN"));
2040    }
2041
2042    #[test]
2043    fn case_reports_missing_then_end_and_empty_branch() {
2044        for input in [
2045            "CASE END",
2046            "CASE WHEN a = 1 'one' END",
2047            "CASE WHEN a = 1 THEN 'one'",
2048        ] {
2049            let mut parser = Parser::new(input).expect("lexer");
2050            assert!(
2051                parser.parse_expr().is_err(),
2052                "expected CASE parse failure for {input}"
2053            );
2054        }
2055    }
2056
2057    #[test]
2058    fn span_tracks_token_range() {
2059        // A literal's span must cover the exact tokens consumed.
2060        let mut parser = Parser::new("123 + 456").expect("lexer");
2061        let e = parser.parse_expr().expect("parse_expr");
2062        let span = e.span();
2063        assert!(!span.is_synthetic(), "root span must be real");
2064        assert!(span.start.offset < span.end.offset);
2065    }
2066
2067    // ====================================================================
2068    // Window OVER clause — issue #589 slice 7a
2069    // ====================================================================
2070
2071    fn try_parse(input: &str) -> Result<Expr, ParseError> {
2072        let mut parser = Parser::new(input).expect("lexer init");
2073        parser.parse_expr()
2074    }
2075
2076    #[test]
2077    fn window_lag_partition_and_order() {
2078        let e = parse("LAG(ts) OVER (PARTITION BY user_id ORDER BY ts)");
2079        let Expr::WindowFunctionCall {
2080            name, args, window, ..
2081        } = e
2082        else {
2083            panic!("expected WindowFunctionCall");
2084        };
2085        assert_eq!(name.to_uppercase(), "LAG");
2086        assert_eq!(args.len(), 1);
2087        assert_eq!(window.partition_by.len(), 1);
2088        assert_eq!(window.order_by.len(), 1);
2089        assert!(window.order_by[0].ascending);
2090        assert!(window.frame.is_none());
2091    }
2092
2093    #[test]
2094    fn window_row_number_empty_over() {
2095        let e = parse("ROW_NUMBER() OVER ()");
2096        let Expr::WindowFunctionCall {
2097            name, args, window, ..
2098        } = e
2099        else {
2100            panic!("expected WindowFunctionCall");
2101        };
2102        assert_eq!(name.to_uppercase(), "ROW_NUMBER");
2103        assert!(args.is_empty());
2104        assert!(window.partition_by.is_empty());
2105        assert!(window.order_by.is_empty());
2106        assert!(window.frame.is_none());
2107    }
2108
2109    #[test]
2110    fn window_sum_with_frame_rows_between() {
2111        let e = parse(
2112            "SUM(amount) OVER (PARTITION BY user_id ORDER BY ts \
2113             ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)",
2114        );
2115        let Expr::WindowFunctionCall { name, window, .. } = e else {
2116            panic!("expected WindowFunctionCall");
2117        };
2118        assert_eq!(name.to_uppercase(), "SUM");
2119        let frame = window.frame.expect("frame present");
2120        assert!(matches!(frame.unit, crate::ast::WindowFrameUnit::Rows));
2121        assert!(matches!(
2122            frame.start,
2123            crate::ast::WindowFrameBound::Preceding(_)
2124        ));
2125        assert!(matches!(
2126            frame.end,
2127            Some(crate::ast::WindowFrameBound::CurrentRow)
2128        ));
2129    }
2130
2131    #[test]
2132    fn window_rank_order_desc_multiple_keys() {
2133        let e = parse("RANK() OVER (ORDER BY score DESC, ts)");
2134        let Expr::WindowFunctionCall { window, .. } = e else {
2135            panic!("expected WindowFunctionCall");
2136        };
2137        assert_eq!(window.order_by.len(), 2);
2138        assert!(!window.order_by[0].ascending);
2139        assert!(window.order_by[1].ascending);
2140    }
2141
2142    #[test]
2143    fn window_unbounded_preceding_following_frame() {
2144        let e = parse(
2145            "AVG(x) OVER (ORDER BY t \
2146             RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)",
2147        );
2148        let Expr::WindowFunctionCall { window, .. } = e else {
2149            panic!("expected WindowFunctionCall");
2150        };
2151        let frame = window.frame.expect("frame present");
2152        assert!(matches!(frame.unit, crate::ast::WindowFrameUnit::Range));
2153        assert!(matches!(
2154            frame.start,
2155            crate::ast::WindowFrameBound::UnboundedPreceding
2156        ));
2157        assert!(matches!(
2158            frame.end,
2159            Some(crate::ast::WindowFrameBound::UnboundedFollowing)
2160        ));
2161    }
2162
2163    #[test]
2164    fn window_rejects_non_window_function() {
2165        // UPPER is a scalar function, not eligible for OVER.
2166        let err = try_parse("UPPER(name) OVER (PARTITION BY id)")
2167            .err()
2168            .expect("should reject scalar OVER");
2169        let msg = err.to_string();
2170        assert!(
2171            msg.contains("UPPER") || msg.contains("upper"),
2172            "error should mention function name, got: {msg}"
2173        );
2174        assert!(msg.to_ascii_uppercase().contains("OVER") || msg.contains("window"));
2175    }
2176
2177    #[test]
2178    fn window_rejects_missing_open_paren() {
2179        let err = try_parse("LAG(ts) OVER PARTITION BY user_id")
2180            .err()
2181            .expect("should reject");
2182        let msg = err.to_string();
2183        assert!(
2184            msg.contains("(") || msg.to_ascii_uppercase().contains("EXPECTED"),
2185            "got: {msg}"
2186        );
2187    }
2188
2189    #[test]
2190    fn window_rejects_invalid_frame_syntax() {
2191        // CURRENT without ROW is malformed.
2192        let err = try_parse("LAG(ts) OVER (ORDER BY ts ROWS CURRENT)")
2193            .err()
2194            .expect("should reject");
2195        let msg = err.to_string();
2196        assert!(
2197            !msg.is_empty(),
2198            "expected non-empty error for malformed frame"
2199        );
2200    }
2201
2202    #[test]
2203    fn window_first_value_with_partition_only() {
2204        let e = parse("FIRST_VALUE(price) OVER (PARTITION BY symbol)");
2205        let Expr::WindowFunctionCall {
2206            name, window, args, ..
2207        } = e
2208        else {
2209            panic!("expected WindowFunctionCall");
2210        };
2211        assert_eq!(name.to_uppercase(), "FIRST_VALUE");
2212        assert_eq!(args.len(), 1);
2213        assert_eq!(window.partition_by.len(), 1);
2214        assert!(window.order_by.is_empty());
2215    }
2216
2217    #[test]
2218    fn window_order_nulls_first_and_last() {
2219        let e = parse("SUM(x) OVER (ORDER BY score ASC NULLS FIRST, ts DESC NULLS LAST)");
2220        let Expr::WindowFunctionCall { window, .. } = e else {
2221            panic!("expected WindowFunctionCall");
2222        };
2223        assert_eq!(window.order_by.len(), 2);
2224        assert!(window.order_by[0].ascending);
2225        assert!(window.order_by[0].nulls_first);
2226        assert!(!window.order_by[1].ascending);
2227        assert!(!window.order_by[1].nulls_first);
2228    }
2229
2230    #[test]
2231    fn window_single_bound_frames() {
2232        let e = parse("SUM(x) OVER (ORDER BY ts ROWS 3 PRECEDING)");
2233        let Expr::WindowFunctionCall { window, .. } = e else {
2234            panic!("expected WindowFunctionCall");
2235        };
2236        let frame = window.frame.expect("frame");
2237        assert!(matches!(
2238            frame.start,
2239            crate::ast::WindowFrameBound::Preceding(_)
2240        ));
2241        assert!(frame.end.is_none());
2242
2243        let e = parse("SUM(x) OVER (ORDER BY ts RANGE 1 FOLLOWING)");
2244        let Expr::WindowFunctionCall { window, .. } = e else {
2245            panic!("expected WindowFunctionCall");
2246        };
2247        let frame = window.frame.expect("frame");
2248        assert!(matches!(
2249            frame.start,
2250            crate::ast::WindowFrameBound::Following(_)
2251        ));
2252        assert!(frame.end.is_none());
2253    }
2254
2255    #[test]
2256    fn window_reports_nulls_and_frame_bound_errors() {
2257        for input in [
2258            "SUM(x) OVER (ORDER BY score NULLS MIDDLE)",
2259            "SUM(x) OVER (ORDER BY score ROWS UNBOUNDED)",
2260            "SUM(x) OVER (ORDER BY score ROWS 3)",
2261        ] {
2262            let err = try_parse(input).expect_err("window syntax should fail");
2263            assert!(!err.to_string().is_empty(), "{input}");
2264        }
2265    }
2266}