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