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