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