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