Skip to main content

rustledger_query/
parser.rs

1//! BQL Parser implementation.
2//!
3//! Uses chumsky for parser combinators.
4
5use chumsky::prelude::*;
6use rust_decimal::Decimal;
7use std::str::FromStr;
8
9use crate::ast::{
10    BalancesQuery, BinaryOperator, ColumnDef, CreateTableStmt, Expr, FromClause, FunctionCall,
11    InsertSource, InsertStmt, JournalQuery, Literal, OrderSpec, PrintQuery, Query, SelectQuery,
12    SortDirection, Target, UnaryOperator, WindowFunction, WindowSpec,
13};
14use crate::error::{ParseError, ParseErrorKind};
15use rustledger_core::NaiveDate;
16
17type ParserInput<'a> = &'a str;
18type ParserExtra<'a> = extra::Err<Rich<'a, char>>;
19
20/// Helper enum for parsing comparison suffix (BETWEEN, IN, or binary comparison).
21enum ComparisonSuffix {
22    Between(Expr, Expr),
23    Binary(BinaryOperator, Expr),
24    /// IN with right-hand side (set literal or expression).
25    In(Expr),
26    /// NOT IN with right-hand side (set literal or expression).
27    NotIn(Expr),
28}
29
30/// Maximum nesting depth of parenthesized expressions / subqueries the
31/// parser accepts. The recursive (chumsky) grammar descends one stack
32/// frame per open paren, so without a bound a deeply-nested input (e.g.
33/// `(((((…`) overflows the stack and parses in super-linear time — a
34/// denial-of-service surfaced by the query fuzzer's slow-unit corpus
35/// (a ~2 KB input of
36/// 1023 nested parens took >10 s and overflowed an 8 MB stack). Real
37/// queries nest only a handful deep; 64 is far above any legitimate
38/// query yet shallow enough to stay fast and stack-safe.
39///
40/// STACK SAFETY: the cap alone is not what keeps recursion safe — frame
41/// sizes drift with toolchains (Rust 1.97's debug frames grew enough
42/// that even 50 levels overflowed a 2 MiB cargo-test thread where 1.96
43/// handled 100+). On native targets [`parse`] therefore runs the
44/// recursive grammar on a dedicated [`PARSE_STACK_SIZE`] thread, making
45/// the depth budget explicit instead of inherited from whichever thread
46/// happens to call it (main = 8 MiB, cargo-test/rayon workers = 2 MiB).
47/// On wasm32 there are no threads; the pre-scan cap and the engine's own
48/// stack limits apply as before.
49const MAX_NESTING_DEPTH: usize = 128;
50
51/// Stack size for the dedicated parse thread on native targets. Sized
52/// for `MAX_NESTING_DEPTH` recursion levels at worst-case (debug-build)
53/// frame sizes with generous headroom.
54#[cfg(not(target_arch = "wasm32"))]
55const PARSE_STACK_SIZE: usize = 16 * 1024 * 1024;
56
57/// Byte offset at which parenthesis nesting first exceeds
58/// [`MAX_NESTING_DEPTH`], or `None` if the input stays within the bound.
59///
60/// Parens inside string literals (`"..."` / `'...'`, with `\` escapes)
61/// are skipped so they don't count — matching the grammar, which treats
62/// them as opaque string bytes rather than expression delimiters.
63fn nesting_exceeds_limit(source: &str) -> Option<usize> {
64    let mut depth: usize = 0;
65    let mut chars = source.char_indices();
66    while let Some((i, c)) = chars.next() {
67        match c {
68            '"' | '\'' => {
69                // Consume the string body so its parens don't count.
70                while let Some((_, sc)) = chars.next() {
71                    if sc == '\\' {
72                        chars.next(); // skip the escaped char
73                    } else if sc == c {
74                        break;
75                    }
76                }
77            }
78            '(' => {
79                depth += 1;
80                if depth > MAX_NESTING_DEPTH {
81                    return Some(i);
82                }
83            }
84            ')' => depth = depth.saturating_sub(1),
85            _ => {}
86        }
87    }
88    None
89}
90
91/// Parse a BQL query string.
92///
93/// # Errors
94///
95/// Returns a `ParseError` if the query string is malformed, or if
96/// parenthesis nesting exceeds the internal nesting limit (rejected up
97/// front to bound parse time and stack depth).
98pub fn parse(source: &str) -> Result<Query, ParseError> {
99    if let Some(offset) = nesting_exceeds_limit(source) {
100        return Err(ParseError::new(
101            ParseErrorKind::SyntaxError(format!(
102                "expression nesting too deep (exceeds maximum of {MAX_NESTING_DEPTH})"
103            )),
104            offset,
105        ));
106    }
107
108    // Run the recursive descent on a thread with an explicit, generous
109    // stack: the caller's stack budget is unknown (2 MiB on cargo-test
110    // and rayon/tokio workers) and grammar frame sizes drift across
111    // toolchains — see the note on `MAX_NESTING_DEPTH`. Scoped so
112    // `source` can be borrowed; the join panics only if the parse thread
113    // panicked, which propagates the panic as before.
114    #[cfg(not(target_arch = "wasm32"))]
115    {
116        std::thread::scope(|scope| {
117            std::thread::Builder::new()
118                .name("bql-parse".to_string())
119                .stack_size(PARSE_STACK_SIZE)
120                .spawn_scoped(scope, || parse_on_current_stack(source))
121                .expect("spawn bql-parse thread")
122                .join()
123                .expect("bql-parse thread panicked")
124        })
125    }
126    #[cfg(target_arch = "wasm32")]
127    {
128        parse_on_current_stack(source)
129    }
130}
131
132/// The recursive grammar proper — must only be called with a known-adequate
133/// stack (the dedicated thread in [`parse`], or wasm's engine stack).
134fn parse_on_current_stack(source: &str) -> Result<Query, ParseError> {
135    let (result, errs) = query_parser()
136        .then_ignore(ws())
137        .then_ignore(end())
138        .parse(source)
139        .into_output_errors();
140
141    if let Some(query) = result {
142        Ok(query)
143    } else {
144        let err = errs.first().map(|e| {
145            let start = e.span().start;
146            let kind = if e.found().is_some() {
147                // chumsky found a concrete unexpected token: keep its rich
148                // message ("expected keyword …", "invalid number", …).
149                ParseErrorKind::SyntaxError(e.to_string())
150            } else if start >= source.len() {
151                // `found() == None` at/past the end is a genuine premature EOF.
152                ParseErrorKind::UnexpectedEof
153            } else if let Some(rest) = source.get(start..) {
154                // `found() == None` mid-input is the `end()` combinator
155                // rejecting leftover tokens after a valid prefix. The span
156                // points at the real token; name it instead of mislabeling it
157                // "unexpected end of input".
158                let token = rest.split_whitespace().next().unwrap_or(rest);
159                ParseErrorKind::SyntaxError(format!("unexpected token '{token}'"))
160            } else {
161                ParseErrorKind::SyntaxError(e.to_string())
162            };
163            ParseError::new(kind, start)
164        });
165        Err(err.unwrap_or_else(|| ParseError::new(ParseErrorKind::UnexpectedEof, 0)))
166    }
167}
168
169/// Parse whitespace (spaces, tabs, newlines).
170fn ws<'a>() -> impl Parser<'a, ParserInput<'a>, (), ParserExtra<'a>> + Clone {
171    one_of(" \t\r\n").repeated().ignored()
172}
173
174/// Parse required whitespace.
175fn ws1<'a>() -> impl Parser<'a, ParserInput<'a>, (), ParserExtra<'a>> + Clone {
176    one_of(" \t\r\n").repeated().at_least(1).ignored()
177}
178
179/// Case-insensitive keyword parser.
180fn kw<'a>(keyword: &'static str) -> impl Parser<'a, ParserInput<'a>, (), ParserExtra<'a>> + Clone {
181    text::ident().try_map(move |s: &str, span| {
182        if s.eq_ignore_ascii_case(keyword) {
183            Ok(())
184        } else {
185            Err(Rich::custom(span, format!("expected keyword '{keyword}'")))
186        }
187    })
188}
189
190/// Parse digits.
191fn digits<'a>() -> impl Parser<'a, ParserInput<'a>, &'a str, ParserExtra<'a>> + Clone {
192    one_of("0123456789").repeated().at_least(1).to_slice()
193}
194
195/// Parse the main query.
196fn query_parser<'a>() -> impl Parser<'a, ParserInput<'a>, Query, ParserExtra<'a>> {
197    ws().ignore_then(choice((
198        create_table_stmt().map(Query::CreateTable),
199        insert_stmt().map(Query::Insert),
200        select_query().map(|sq| Query::Select(Box::new(sq))),
201        journal_query().map(Query::Journal),
202        balances_query().map(Query::Balances),
203        print_query().map(Query::Print),
204    )))
205    .then_ignore(ws())
206    .then_ignore(just(';').or_not())
207}
208
209/// Parse a SELECT query with optional subquery support.
210fn select_query<'a>() -> impl Parser<'a, ParserInput<'a>, SelectQuery, ParserExtra<'a>> {
211    recursive(|select_parser| {
212        // Subquery in FROM clause: FROM (SELECT ...)
213        let subquery_from = ws1()
214            .ignore_then(kw("FROM"))
215            .ignore_then(ws1())
216            .ignore_then(just('('))
217            .ignore_then(ws())
218            .ignore_then(select_parser)
219            .then_ignore(ws())
220            .then_ignore(just(')'))
221            .map(|sq| Some(FromClause::from_subquery(sq)));
222
223        // Table name FROM clause: FROM tablename (where tablename is not a keyword)
224        // A table name is an identifier followed by WHERE/GROUP/ORDER/HAVING/LIMIT/PIVOT or end
225        // Supports system tables like #prices, #entries
226        let table_from = ws1()
227            .ignore_then(kw("FROM"))
228            .ignore_then(ws1())
229            .ignore_then(table_identifier().try_map(|name, span| {
230                // Check if this looks like a table name (uppercase convention or doesn't look like account)
231                // Table names should not contain ':' which accounts have
232                // System tables starting with '#' are always valid
233                if !name.starts_with('#') && name.contains(':') {
234                    Err(Rich::custom(
235                        span,
236                        "table names cannot contain ':' - this looks like an account filter expression",
237                    ))
238                } else {
239                    Ok(name)
240                }
241            }))
242            .then_ignore(
243                // Must be followed by WHERE, GROUP, ORDER, HAVING, LIMIT, PIVOT, or end
244                ws().then(choice((
245                    kw("WHERE").ignored(),
246                    kw("GROUP").ignored(),
247                    kw("ORDER").ignored(),
248                    kw("HAVING").ignored(),
249                    kw("LIMIT").ignored(),
250                    kw("PIVOT").ignored(),
251                    end().ignored(),
252                )))
253                .rewind(),
254            )
255            .map(|name| Some(FromClause::from_table(name)));
256
257        // Regular FROM clause
258        let regular_from = from_clause().map(Some);
259
260        kw("SELECT")
261            .ignore_then(ws1())
262            .ignore_then(
263                kw("DISTINCT")
264                    .then_ignore(ws())
265                    .or_not()
266                    .map(|d| d.is_some()),
267            )
268            .then(targets())
269            .then(
270                subquery_from
271                    .or(table_from)
272                    .or(regular_from)
273                    .or_not()
274                    .map(std::option::Option::flatten),
275            )
276            .then(where_clause().or_not())
277            .then(group_by_clause().or_not())
278            .then(having_clause().or_not())
279            // Clause order matches bean-query (#1034): ORDER BY before
280            // PIVOT BY. Pre-#1034 rledger had PIVOT BY before ORDER BY,
281            // which is bean-query-incompatible — flipping the order here
282            // gives upstream parity for queries that use both.
283            .then(order_by_clause().or_not())
284            .then(pivot_by_clause().or_not())
285            .then(limit_clause().or_not())
286            .map(
287                |(
288                    (
289                        (
290                            (((((distinct, targets), from), where_clause), group_by), having),
291                            order_by,
292                        ),
293                        pivot_by,
294                    ),
295                    limit,
296                )| {
297                    SelectQuery {
298                        distinct,
299                        targets,
300                        from,
301                        where_clause,
302                        group_by,
303                        having,
304                        pivot_by,
305                        order_by,
306                        limit,
307                    }
308                },
309            )
310    })
311}
312
313/// Parse FROM clause.
314fn from_clause<'a>() -> impl Parser<'a, ParserInput<'a>, FromClause, ParserExtra<'a>> + Clone {
315    ws1()
316        .ignore_then(kw("FROM"))
317        .ignore_then(ws1())
318        .ignore_then(from_modifiers())
319}
320
321/// Parse target expressions.
322fn targets<'a>() -> impl Parser<'a, ParserInput<'a>, Vec<Target>, ParserExtra<'a>> + Clone {
323    target()
324        .separated_by(ws().then(just(',')).then(ws()))
325        .at_least(1)
326        .collect()
327}
328
329/// Parse a single target.
330fn target<'a>() -> impl Parser<'a, ParserInput<'a>, Target, ParserExtra<'a>> + Clone {
331    expr()
332        .then(
333            ws1()
334                .ignore_then(kw("AS"))
335                .ignore_then(ws1())
336                .ignore_then(identifier())
337                .or_not(),
338        )
339        .map(|(expr, alias)| Target { expr, alias })
340}
341
342/// Parse FROM modifiers (OPEN ON, CLOSE ON, CLEAR, filter).
343fn from_modifiers<'a>() -> impl Parser<'a, ParserInput<'a>, FromClause, ParserExtra<'a>> + Clone {
344    // Each modifier consumes its LEADING separator whitespace and never its
345    // trailing whitespace, so the boundary space before a following clause
346    // (`ORDER BY`/`GROUP BY`/`WHERE`/…) is left intact for that clause's
347    // required `ws1()`. (Previously each modifier ate its trailing whitespace,
348    // which starved the following clause and turned
349    // `FROM CLOSE ON <date> ORDER BY …` into a syntax error.) `open_on` is
350    // always first, so its leading whitespace is already consumed by `FROM ` in
351    // `from_clause`.
352    let open_on = kw("OPEN")
353        .ignore_then(ws1())
354        .ignore_then(kw("ON"))
355        .ignore_then(ws1())
356        .ignore_then(date_literal());
357
358    let close_on = ws()
359        .ignore_then(kw("CLOSE"))
360        .ignore_then(ws().then(kw("ON")).then(ws()).or_not())
361        .ignore_then(date_literal());
362
363    let clear = ws().ignore_then(kw("CLEAR"));
364
365    // Parse modifiers in order: OPEN ON, CLOSE ON, CLEAR, filter
366    // Or just a table name for user-created tables
367    open_on
368        .or_not()
369        .then(close_on.or_not())
370        .then(clear.or_not().map(|c| c.is_some()))
371        .then(from_filter().or_not())
372        .map(|(((open_on, close_on), clear), filter)| FromClause {
373            open_on,
374            close_on,
375            clear,
376            filter,
377            subquery: None,
378            table_name: None,
379        })
380}
381
382/// Parse FROM filter expression (predicates).
383///
384/// A FROM filter is a bare expression preceded by separator whitespace, but it
385/// must not start with a following-clause keyword (`WHERE`/`GROUP`/`ORDER`/
386/// `HAVING`/`LIMIT`/`PIVOT`) — otherwise the optional filter greedily parses
387/// e.g. the `ORDER` in `FROM CLOSE ON <date> ORDER BY …` as a column reference,
388/// consuming the keyword and producing a syntax error. Peek past the leading
389/// whitespace; if a clause keyword follows, decline (leaving the boundary for
390/// that clause).
391fn from_filter<'a>() -> impl Parser<'a, ParserInput<'a>, Expr, ParserExtra<'a>> + Clone {
392    let clause_keyword = choice((
393        kw("WHERE").ignored(),
394        kw("GROUP").ignored(),
395        kw("ORDER").ignored(),
396        kw("HAVING").ignored(),
397        kw("LIMIT").ignored(),
398        kw("PIVOT").ignored(),
399    ));
400    ws().ignore_then(clause_keyword.not().rewind())
401        .ignore_then(expr())
402}
403
404/// Parse WHERE clause.
405fn where_clause<'a>() -> impl Parser<'a, ParserInput<'a>, Expr, ParserExtra<'a>> + Clone {
406    ws1()
407        .ignore_then(kw("WHERE"))
408        .ignore_then(ws1())
409        .ignore_then(expr())
410}
411
412/// Parse GROUP BY clause.
413fn group_by_clause<'a>() -> impl Parser<'a, ParserInput<'a>, Vec<Expr>, ParserExtra<'a>> + Clone {
414    ws1()
415        .ignore_then(kw("GROUP"))
416        .ignore_then(ws1())
417        .ignore_then(kw("BY"))
418        .ignore_then(ws1())
419        .ignore_then(
420            expr()
421                .separated_by(ws().then(just(',')).then(ws()))
422                .at_least(1)
423                .collect(),
424        )
425}
426
427/// Parse HAVING clause (filter on aggregated results).
428fn having_clause<'a>() -> impl Parser<'a, ParserInput<'a>, Expr, ParserExtra<'a>> + Clone {
429    ws1()
430        .ignore_then(kw("HAVING"))
431        .ignore_then(ws1())
432        .ignore_then(expr())
433}
434
435/// Parse PIVOT BY clause (pivot table transformation).
436fn pivot_by_clause<'a>() -> impl Parser<'a, ParserInput<'a>, Vec<Expr>, ParserExtra<'a>> + Clone {
437    ws1()
438        .ignore_then(kw("PIVOT"))
439        .ignore_then(ws1())
440        .ignore_then(kw("BY"))
441        .ignore_then(ws1())
442        .ignore_then(
443            expr()
444                .separated_by(ws().then(just(',')).then(ws()))
445                .at_least(1)
446                .collect(),
447        )
448}
449
450/// Parse ORDER BY clause.
451fn order_by_clause<'a>() -> impl Parser<'a, ParserInput<'a>, Vec<OrderSpec>, ParserExtra<'a>> + Clone
452{
453    ws1()
454        .ignore_then(kw("ORDER"))
455        .ignore_then(ws1())
456        .ignore_then(kw("BY"))
457        .ignore_then(ws1())
458        .ignore_then(
459            order_spec()
460                .separated_by(ws().then(just(',')).then(ws()))
461                .at_least(1)
462                .collect(),
463        )
464}
465
466/// Parse a single ORDER BY spec.
467fn order_spec<'a>() -> impl Parser<'a, ParserInput<'a>, OrderSpec, ParserExtra<'a>> + Clone {
468    expr()
469        .then(
470            ws1()
471                .ignore_then(choice((
472                    kw("ASC").to(SortDirection::Asc),
473                    kw("DESC").to(SortDirection::Desc),
474                )))
475                .or_not(),
476        )
477        .map(|(expr, dir)| OrderSpec {
478            expr,
479            direction: dir.unwrap_or_default(),
480        })
481}
482
483/// Parse LIMIT clause.
484fn limit_clause<'a>() -> impl Parser<'a, ParserInput<'a>, u64, ParserExtra<'a>> + Clone {
485    ws1()
486        .ignore_then(kw("LIMIT"))
487        .ignore_then(ws1())
488        .ignore_then(integer())
489        .map(|n| n as u64)
490}
491
492/// Parse JOURNAL query.
493fn journal_query<'a>() -> impl Parser<'a, ParserInput<'a>, JournalQuery, ParserExtra<'a>> + Clone {
494    kw("JOURNAL")
495        .ignore_then(
496            // Account pattern is optional - can be JOURNAL or JOURNAL "pattern"
497            ws1().ignore_then(string_literal()).or_not(),
498        )
499        .then(at_function().or_not())
500        .then(
501            ws1()
502                .ignore_then(kw("FROM"))
503                .ignore_then(ws1())
504                .ignore_then(from_modifiers())
505                .or_not(),
506        )
507        .map(|((account_pattern, at_function), from)| JournalQuery {
508            account_pattern: account_pattern.unwrap_or_default(),
509            at_function,
510            from,
511        })
512}
513
514/// Parse BALANCES query.
515fn balances_query<'a>() -> impl Parser<'a, ParserInput<'a>, BalancesQuery, ParserExtra<'a>> + Clone
516{
517    // Use rewind-based lookahead so optional clauses don't consume whitespace
518    // that subsequent clauses need. Without this, `BALANCES WHERE ...` fails
519    // because at_function() consumes whitespace before failing on "WHERE" != "AT".
520    let at_fn = ws1().then(kw("AT")).rewind().ignore_then(at_function());
521
522    let from = ws1().then(kw("FROM")).rewind().ignore_then(
523        ws1()
524            .ignore_then(kw("FROM"))
525            .ignore_then(ws1())
526            .ignore_then(from_modifiers()),
527    );
528
529    kw("BALANCES")
530        .ignore_then(at_fn.or_not())
531        .then(from.or_not())
532        .then(where_clause().or_not())
533        .map(|((at_function, from), where_clause)| BalancesQuery {
534            at_function,
535            from,
536            where_clause,
537        })
538}
539
540/// Parse PRINT query.
541fn print_query<'a>() -> impl Parser<'a, ParserInput<'a>, PrintQuery, ParserExtra<'a>> + Clone {
542    kw("PRINT")
543        .ignore_then(
544            ws1()
545                .ignore_then(kw("FROM"))
546                .ignore_then(ws1())
547                .ignore_then(from_modifiers())
548                .or_not(),
549        )
550        .map(|from| PrintQuery { from })
551}
552
553/// Parse CREATE TABLE statement.
554fn create_table_stmt<'a>() -> impl Parser<'a, ParserInput<'a>, CreateTableStmt, ParserExtra<'a>> {
555    // CREATE TABLE name (col1, col2, ...) or CREATE TABLE name AS SELECT ...
556    let column_def = identifier()
557        .then(ws().ignore_then(identifier()).or_not())
558        .map(|(name, type_hint)| ColumnDef { name, type_hint });
559
560    let column_list = just('(')
561        .ignore_then(ws())
562        .ignore_then(
563            column_def
564                .separated_by(ws().ignore_then(just(',')).then_ignore(ws()))
565                .collect::<Vec<_>>(),
566        )
567        .then_ignore(ws())
568        .then_ignore(just(')'));
569
570    let as_select = ws1()
571        .ignore_then(kw("AS"))
572        .ignore_then(ws1())
573        .ignore_then(select_query())
574        .map(Box::new);
575
576    kw("CREATE")
577        .ignore_then(ws1())
578        .ignore_then(kw("TABLE"))
579        .ignore_then(ws1())
580        .ignore_then(identifier())
581        .then(ws().ignore_then(column_list).or_not())
582        .then(as_select.or_not())
583        .map(|((table_name, columns), as_select)| CreateTableStmt {
584            table_name,
585            columns: columns.unwrap_or_default(),
586            as_select,
587        })
588}
589
590/// Parse INSERT statement.
591fn insert_stmt<'a>() -> impl Parser<'a, ParserInput<'a>, InsertStmt, ParserExtra<'a>> {
592    // Column list: (col1, col2, ...)
593    let column_list = just('(')
594        .ignore_then(ws())
595        .ignore_then(
596            identifier()
597                .separated_by(ws().ignore_then(just(',')).then_ignore(ws()))
598                .collect::<Vec<_>>(),
599        )
600        .then_ignore(ws())
601        .then_ignore(just(')'));
602
603    // VALUES clause: VALUES (v1, v2), (v3, v4), ...
604    let value_row = just('(')
605        .ignore_then(ws())
606        .ignore_then(
607            expr()
608                .separated_by(ws().ignore_then(just(',')).then_ignore(ws()))
609                .collect::<Vec<_>>(),
610        )
611        .then_ignore(ws())
612        .then_ignore(just(')'));
613
614    let values_source = kw("VALUES")
615        .ignore_then(ws())
616        .ignore_then(
617            value_row
618                .separated_by(ws().ignore_then(just(',')).then_ignore(ws()))
619                .collect::<Vec<_>>(),
620        )
621        .map(InsertSource::Values);
622
623    // SELECT as source
624    let select_source = select_query().map(|sq| InsertSource::Select(Box::new(sq)));
625
626    let source = choice((values_source, select_source));
627
628    kw("INSERT")
629        .ignore_then(ws1())
630        .ignore_then(kw("INTO"))
631        .ignore_then(ws1())
632        .ignore_then(identifier())
633        .then(ws().ignore_then(column_list).or_not())
634        .then_ignore(ws())
635        .then(source)
636        .map(|((table_name, columns), source)| InsertStmt {
637            table_name,
638            columns,
639            source,
640        })
641}
642
643/// Parse AT function (e.g., AT cost, AT units).
644fn at_function<'a>() -> impl Parser<'a, ParserInput<'a>, String, ParserExtra<'a>> + Clone {
645    ws1()
646        .ignore_then(kw("AT"))
647        .ignore_then(ws1())
648        .ignore_then(identifier())
649}
650
651/// Parse an expression (with precedence climbing).
652#[allow(clippy::large_stack_frames)]
653fn expr<'a>() -> Boxed<'a, 'a, ParserInput<'a>, Expr, ParserExtra<'a>> {
654    recursive(|expr| {
655        let primary = primary_expr(expr.clone()).boxed();
656
657        // Unary minus
658        let unary = just('-')
659            .then_ignore(ws())
660            .or_not()
661            .then(primary)
662            .map(|(neg, e)| {
663                if neg.is_some() {
664                    Expr::unary(UnaryOperator::Neg, e)
665                } else {
666                    e
667                }
668            })
669            .boxed();
670
671        // Multiplicative: * / %
672        let multiplicative = unary
673            .clone()
674            .foldl(
675                ws().ignore_then(choice((
676                    just('*').to(BinaryOperator::Mul),
677                    just('/').to(BinaryOperator::Div),
678                    just('%').to(BinaryOperator::Mod),
679                )))
680                .then_ignore(ws())
681                .then(unary)
682                .repeated(),
683                |left, (op, right)| Expr::binary(left, op, right),
684            )
685            .boxed();
686
687        // Additive: + -
688        let additive = multiplicative
689            .clone()
690            .foldl(
691                ws().ignore_then(choice((
692                    just('+').to(BinaryOperator::Add),
693                    just('-').to(BinaryOperator::Sub),
694                )))
695                .then_ignore(ws())
696                .then(multiplicative)
697                .repeated(),
698                |left, (op, right)| Expr::binary(left, op, right),
699            )
700            .boxed();
701
702        // Comparison: = != < <= > >= ~ !~ IN NOT IN BETWEEN IS NULL
703        let comparison = additive
704            .clone()
705            .then(
706                choice((
707                    // BETWEEN ... AND
708                    ws1()
709                        .ignore_then(kw("BETWEEN"))
710                        .ignore_then(ws1())
711                        .ignore_then(additive.clone())
712                        .then_ignore(ws1())
713                        .then_ignore(kw("AND"))
714                        .then_ignore(ws1())
715                        .then(additive.clone())
716                        .map(|(low, high)| ComparisonSuffix::Between(low, high)),
717                    // NOT IN - try set literal first, then fall back to expression
718                    ws1()
719                        .ignore_then(kw("NOT"))
720                        .ignore_then(ws1())
721                        .ignore_then(kw("IN"))
722                        .ignore_then(ws())
723                        .ignore_then(choice((
724                            set_literal(expr.clone()),
725                            additive.clone(),
726                        )))
727                        .map(ComparisonSuffix::NotIn),
728                    // IN - try set literal first, then fall back to expression
729                    ws1()
730                        .ignore_then(kw("IN"))
731                        .ignore_then(ws())
732                        .ignore_then(choice((
733                            set_literal(expr.clone()),
734                            additive.clone(),
735                        )))
736                        .map(ComparisonSuffix::In),
737                    // Regular comparison operators
738                    ws()
739                        .ignore_then(comparison_op())
740                        .then_ignore(ws())
741                        .then(additive)
742                        .map(|(op, right)| ComparisonSuffix::Binary(op, right)),
743                ))
744                .or_not(),
745            )
746            .map(|(left, suffix)| match suffix {
747                Some(ComparisonSuffix::Between(low, high)) => Expr::between(left, low, high),
748                Some(ComparisonSuffix::Binary(op, right)) => Expr::binary(left, op, right),
749                Some(ComparisonSuffix::In(right)) => Expr::binary(left, BinaryOperator::In, right),
750                Some(ComparisonSuffix::NotIn(right)) => {
751                    Expr::binary(left, BinaryOperator::NotIn, right)
752                }
753                None => left,
754            })
755            // IS NULL / IS NOT NULL (postfix)
756            .then(
757                ws1()
758                    .ignore_then(kw("IS"))
759                    .ignore_then(ws1())
760                    .ignore_then(choice((
761                        kw("NOT")
762                            .ignore_then(ws1())
763                            .ignore_then(kw("NULL"))
764                            .to(UnaryOperator::IsNotNull),
765                        kw("NULL").to(UnaryOperator::IsNull),
766                    )))
767                    .or_not(),
768            )
769            .map(|(expr, is_null)| {
770                if let Some(op) = is_null {
771                    Expr::unary(op, expr)
772                } else {
773                    expr
774                }
775            })
776            .boxed();
777
778        // NOT
779        let not_expr = kw("NOT")
780            .ignore_then(ws1())
781            .repeated()
782            .collect::<Vec<_>>()
783            .then(comparison)
784            .map(|(nots, e)| {
785                nots.into_iter()
786                    .fold(e, |acc, ()| Expr::unary(UnaryOperator::Not, acc))
787            })
788            .boxed();
789
790        // AND
791        let and_expr = not_expr
792            .clone()
793            .foldl(
794                ws1()
795                    .ignore_then(kw("AND"))
796                    .ignore_then(ws1())
797                    .ignore_then(not_expr)
798                    .repeated(),
799                |left, right| Expr::binary(left, BinaryOperator::And, right),
800            )
801            .boxed();
802
803        // OR (lowest precedence)
804        and_expr.clone().foldl(
805            ws1()
806                .ignore_then(kw("OR"))
807                .ignore_then(ws1())
808                .ignore_then(and_expr)
809                .repeated(),
810            |left, right| Expr::binary(left, BinaryOperator::Or, right),
811        )
812    })
813    .boxed()
814}
815
816/// Parse comparison operators (excluding IN/NOT IN which are handled specially).
817fn comparison_op<'a>() -> impl Parser<'a, ParserInput<'a>, BinaryOperator, ParserExtra<'a>> + Clone
818{
819    choice((
820        // Multi-char operators first
821        just("!=").to(BinaryOperator::Ne),
822        just("!~").to(BinaryOperator::NotRegex),
823        just("<=").to(BinaryOperator::Le),
824        just(">=").to(BinaryOperator::Ge),
825        // Single-char operators
826        just('=').to(BinaryOperator::Eq),
827        just('<').to(BinaryOperator::Lt),
828        just('>').to(BinaryOperator::Gt),
829        just('~').to(BinaryOperator::Regex),
830    ))
831}
832
833/// Parse a set literal for IN operator, e.g., `('EUR', 'USD')`.
834///
835/// To distinguish from parenthesized expressions like `IN (tags)`, set literals
836/// require either:
837/// - Two or more comma-separated elements: `('EUR', 'USD')`
838/// - A single element with trailing comma: `('EUR',)`
839///
840/// This ensures `IN (tags)` is parsed as `IN <parenthesized-column>` rather than
841/// `IN <single-element-set>`.
842fn set_literal<'a>(
843    expr: impl Parser<'a, ParserInput<'a>, Expr, ParserExtra<'a>> + Clone + 'a,
844) -> impl Parser<'a, ParserInput<'a>, Expr, ParserExtra<'a>> + Clone {
845    just('(')
846        .ignore_then(ws())
847        .ignore_then(
848            // Parse first element
849            expr.clone()
850                .then(
851                    // Then require either:
852                    // - comma + more elements (with optional trailing comma)
853                    // - trailing comma (for single-element sets)
854                    ws().ignore_then(just(',')).ignore_then(ws()).ignore_then(
855                        expr.separated_by(ws().then(just(',')).then(ws()))
856                            .allow_trailing()
857                            .collect::<Vec<_>>(),
858                    ),
859                )
860                .map(|(first, rest)| {
861                    let mut elements = Vec::with_capacity(1 + rest.len());
862                    elements.push(first);
863                    elements.extend(rest);
864                    elements
865                }),
866        )
867        .then_ignore(ws())
868        .then_ignore(just(')'))
869        .map(Expr::Set)
870}
871
872/// Parse primary expressions.
873fn primary_expr<'a>(
874    expr: impl Parser<'a, ParserInput<'a>, Expr, ParserExtra<'a>> + Clone + 'a,
875) -> impl Parser<'a, ParserInput<'a>, Expr, ParserExtra<'a>> + Clone {
876    choice((
877        // Parenthesized expression
878        just('(')
879            .ignore_then(ws())
880            .ignore_then(expr.clone())
881            .then_ignore(ws())
882            .then_ignore(just(')'))
883            .map(|e| Expr::Paren(Box::new(e))),
884        // Literals MUST come before the column/function branch: a bare
885        // identifier otherwise greedily parses `TRUE`/`FALSE`/`NULL` as a column
886        // name, so those literals were unreachable in expression position. The
887        // literal parser only matches the reserved keywords plus number/string/
888        // date literals, so ordinary column and function names still fall
889        // through to `function_call_or_column`.
890        literal().map(Expr::Literal),
891        // Function call or column reference (must come before wildcard check)
892        // Pass expr to allow nested function calls like units(sum(position))
893        function_call_or_column(expr),
894        // Wildcard (fallback if nothing else matched)
895        just('*').to(Expr::Wildcard),
896    ))
897}
898
899/// Parse function call, window function, or column reference.
900fn function_call_or_column<'a>(
901    expr: impl Parser<'a, ParserInput<'a>, Expr, ParserExtra<'a>> + Clone + 'a,
902) -> impl Parser<'a, ParserInput<'a>, Expr, ParserExtra<'a>> + Clone {
903    identifier()
904        .then(
905            ws().ignore_then(just('('))
906                .ignore_then(ws())
907                .ignore_then(function_args(expr))
908                .then_ignore(ws())
909                .then_ignore(just(')'))
910                .or_not(),
911        )
912        .then(
913            // Optional OVER clause for window functions
914            ws1()
915                .ignore_then(kw("OVER"))
916                .ignore_then(ws())
917                .ignore_then(just('('))
918                .ignore_then(ws())
919                .ignore_then(window_spec())
920                .then_ignore(ws())
921                .then_ignore(just(')'))
922                .or_not(),
923        )
924        .map(|((name, args), over)| {
925            if let Some(args) = args {
926                if let Some(window_spec) = over {
927                    // Window function
928                    Expr::Window(WindowFunction {
929                        name,
930                        args,
931                        over: window_spec,
932                    })
933                } else {
934                    // Regular function
935                    Expr::Function(FunctionCall { name, args })
936                }
937            } else {
938                Expr::Column(name)
939            }
940        })
941}
942
943/// Parse window specification (PARTITION BY and ORDER BY).
944fn window_spec<'a>() -> impl Parser<'a, ParserInput<'a>, WindowSpec, ParserExtra<'a>> + Clone {
945    let partition_by = kw("PARTITION")
946        .ignore_then(ws1())
947        .ignore_then(kw("BY"))
948        .ignore_then(ws1())
949        .ignore_then(
950            simple_arg()
951                .separated_by(ws().then(just(',')).then(ws()))
952                .at_least(1)
953                .collect::<Vec<_>>(),
954        )
955        .then_ignore(ws());
956
957    let window_order_by = kw("ORDER")
958        .ignore_then(ws1())
959        .ignore_then(kw("BY"))
960        .ignore_then(ws1())
961        .ignore_then(
962            window_order_spec()
963                .separated_by(ws().then(just(',')).then(ws()))
964                .at_least(1)
965                .collect::<Vec<_>>(),
966        );
967
968    partition_by
969        .or_not()
970        .then(window_order_by.or_not())
971        .map(|(partition_by, order_by)| WindowSpec {
972            partition_by,
973            order_by,
974        })
975}
976
977/// Parse ORDER BY spec within window (simple version).
978fn window_order_spec<'a>() -> impl Parser<'a, ParserInput<'a>, OrderSpec, ParserExtra<'a>> + Clone {
979    simple_arg()
980        .then(
981            ws1()
982                .ignore_then(choice((
983                    kw("ASC").to(SortDirection::Asc),
984                    kw("DESC").to(SortDirection::Desc),
985                )))
986                .or_not(),
987        )
988        .map(|(expr, dir)| OrderSpec {
989            expr,
990            direction: dir.unwrap_or_default(),
991        })
992}
993
994/// Parse function arguments.
995fn function_args<'a>(
996    expr: impl Parser<'a, ParserInput<'a>, Expr, ParserExtra<'a>> + Clone + 'a,
997) -> impl Parser<'a, ParserInput<'a>, Vec<Expr>, ParserExtra<'a>> + Clone {
998    // Allow empty args or comma-separated full expressions
999    // This enables nested function calls like units(sum(position))
1000    expr.separated_by(ws().then(just(',')).then(ws())).collect()
1001}
1002
1003/// Parse a simple function argument (column, wildcard, or literal).
1004fn simple_arg<'a>() -> impl Parser<'a, ParserInput<'a>, Expr, ParserExtra<'a>> + Clone {
1005    choice((
1006        just('*').to(Expr::Wildcard),
1007        identifier().map(Expr::Column),
1008        literal().map(Expr::Literal),
1009    ))
1010}
1011
1012/// Parse a literal.
1013fn literal<'a>() -> impl Parser<'a, ParserInput<'a>, Literal, ParserExtra<'a>> + Clone {
1014    choice((
1015        // Keywords first
1016        kw("TRUE").to(Literal::Boolean(true)),
1017        kw("FALSE").to(Literal::Boolean(false)),
1018        kw("NULL").to(Literal::Null),
1019        // Date literal (must be before number to avoid parsing year as number)
1020        date_literal().map(Literal::Date),
1021        // Number — Integer if no decimal point, Number otherwise
1022        number_literal(),
1023        // String
1024        string_literal().map(Literal::String),
1025    ))
1026}
1027
1028/// Parse an identifier (column name, function name).
1029fn identifier<'a>() -> impl Parser<'a, ParserInput<'a>, String, ParserExtra<'a>> + Clone {
1030    text::ident().map(|s: &str| s.to_string())
1031}
1032
1033/// Parse a table identifier, which can be a regular identifier or a system table
1034/// starting with `#` (e.g., `#prices`, `#entries`).
1035fn table_identifier<'a>() -> impl Parser<'a, ParserInput<'a>, String, ParserExtra<'a>> + Clone {
1036    choice((
1037        // System table: #identifier (e.g., #prices)
1038        just('#')
1039            .ignore_then(text::ident())
1040            .map(|s: &str| format!("#{s}")),
1041        // Regular table identifier
1042        text::ident().map(|s: &str| s.to_string()),
1043    ))
1044}
1045
1046/// Parse a string literal.
1047fn string_literal<'a>() -> impl Parser<'a, ParserInput<'a>, String, ParserExtra<'a>> + Clone {
1048    // Double-quoted string
1049    let double_quoted = just('"')
1050        .ignore_then(
1051            none_of("\"\\")
1052                .or(just('\\').ignore_then(any()))
1053                .repeated()
1054                .collect::<String>(),
1055        )
1056        .then_ignore(just('"'));
1057
1058    // Single-quoted string (SQL-style)
1059    let single_quoted = just('\'')
1060        .ignore_then(
1061            none_of("'\\")
1062                .or(just('\\').ignore_then(any()))
1063                .repeated()
1064                .collect::<String>(),
1065        )
1066        .then_ignore(just('\''));
1067
1068    choice((double_quoted, single_quoted))
1069}
1070
1071/// Parse a date literal (YYYY-MM-DD).
1072fn date_literal<'a>() -> impl Parser<'a, ParserInput<'a>, NaiveDate, ParserExtra<'a>> + Clone {
1073    digits()
1074        .then_ignore(just('-'))
1075        .then(digits())
1076        .then_ignore(just('-'))
1077        .then(digits())
1078        .try_map(|((year, month), day): ((&str, &str), &str), span| {
1079            let year: i32 = year
1080                .parse()
1081                .map_err(|_| Rich::custom(span, "invalid year"))?;
1082            let month: u32 = month
1083                .parse()
1084                .map_err(|_| Rich::custom(span, "invalid month"))?;
1085            let day: u32 = day.parse().map_err(|_| Rich::custom(span, "invalid day"))?;
1086            rustledger_core::naive_date(year, month, day)
1087                .ok_or_else(|| Rich::custom(span, "invalid date"))
1088        })
1089}
1090
1091/// Parse a numeric literal as either `Literal::Integer` (no fractional part) or
1092/// `Literal::Number` (has a fractional part, or whole-number value exceeds i64).
1093///
1094/// Distinguishing integer from decimal at the parser level matches BQL/SQL
1095/// semantics and lets functions that strictly require integer arguments
1096/// (e.g. `ROOT(account, n)`, `SUBSTR(s, start, len)`) work with literal
1097/// arguments. See issue #938.
1098fn number_literal<'a>() -> impl Parser<'a, ParserInput<'a>, Literal, ParserExtra<'a>> + Clone {
1099    just('-')
1100        .or_not()
1101        .then(digits())
1102        .then(just('.').then(digits()).or_not())
1103        .try_map(
1104            |((neg, int_part), frac_part): ((Option<char>, &str), Option<(char, &str)>), span| {
1105                let mut s = String::new();
1106                if neg.is_some() {
1107                    s.push('-');
1108                }
1109                s.push_str(int_part);
1110                match frac_part {
1111                    None => match s.parse::<i64>() {
1112                        Ok(i) => Ok(Literal::Integer(i)),
1113                        // Whole-number value out of i64 range — fall back to Decimal.
1114                        Err(_) => Decimal::from_str(&s)
1115                            .map(Literal::Number)
1116                            .map_err(|_| Rich::custom(span, "invalid number")),
1117                    },
1118                    Some((_, frac)) => {
1119                        s.push('.');
1120                        s.push_str(frac);
1121                        Decimal::from_str(&s)
1122                            .map(Literal::Number)
1123                            .map_err(|_| Rich::custom(span, "invalid number"))
1124                    }
1125                }
1126            },
1127        )
1128}
1129
1130/// Parse an integer.
1131fn integer<'a>() -> impl Parser<'a, ParserInput<'a>, i64, ParserExtra<'a>> + Clone {
1132    digits().try_map(|s: &str, span| {
1133        s.parse::<i64>()
1134            .map_err(|_| Rich::custom(span, "invalid integer"))
1135    })
1136}
1137
1138#[cfg(test)]
1139mod tests {
1140    use super::*;
1141    use rust_decimal_macros::dec;
1142
1143    #[test]
1144    fn test_trailing_tokens_are_named_not_mislabeled_eof() {
1145        // Regression for OUTSTANDING #16: leftover tokens after a valid prefix
1146        // were reported as "unexpected end of input" (because `end()` rejects
1147        // them with `found() == None`), even though the span points at the real
1148        // token. They must now be named.
1149        for (q, token, pos) in [
1150            ("SELECT account FOOBAR", "FOOBAR", 15usize),
1151            (
1152                "SELECT account, sum(position) GROUP BY account WHERE number > 0",
1153                "WHERE",
1154                47,
1155            ),
1156        ] {
1157            let err = parse(q).expect_err("should be a parse error");
1158            assert_eq!(err.position, pos, "span should point at the token in {q:?}");
1159            let ParseErrorKind::SyntaxError(ref m) = err.kind else {
1160                panic!(
1161                    "expected SyntaxError naming {token:?}, got {:?} for {q:?}",
1162                    err.kind
1163                );
1164            };
1165            assert!(
1166                m.contains(token),
1167                "error should name {token:?}, got {m:?} for {q:?}"
1168            );
1169        }
1170    }
1171
1172    #[test]
1173    fn test_simple_select() {
1174        let query = parse("SELECT * FROM year = 2024").unwrap();
1175        match query {
1176            Query::Select(sel) => {
1177                assert!(!sel.distinct);
1178                assert_eq!(sel.targets.len(), 1);
1179                assert!(matches!(sel.targets[0].expr, Expr::Wildcard));
1180                assert!(sel.from.is_some());
1181            }
1182            _ => panic!("Expected SELECT query"),
1183        }
1184    }
1185
1186    #[test]
1187    fn test_select_columns() {
1188        let query = parse("SELECT date, account, position").unwrap();
1189        match query {
1190            Query::Select(sel) => {
1191                assert_eq!(sel.targets.len(), 3);
1192                assert!(matches!(&sel.targets[0].expr, Expr::Column(c) if c == "date"));
1193                assert!(matches!(&sel.targets[1].expr, Expr::Column(c) if c == "account"));
1194                assert!(matches!(&sel.targets[2].expr, Expr::Column(c) if c == "position"));
1195            }
1196            _ => panic!("Expected SELECT query"),
1197        }
1198    }
1199
1200    #[test]
1201    fn test_select_with_alias() {
1202        let query = parse("SELECT SUM(position) AS total").unwrap();
1203        match query {
1204            Query::Select(sel) => {
1205                assert_eq!(sel.targets.len(), 1);
1206                assert_eq!(sel.targets[0].alias, Some("total".to_string()));
1207                match &sel.targets[0].expr {
1208                    Expr::Function(f) => {
1209                        assert_eq!(f.name, "SUM");
1210                        assert_eq!(f.args.len(), 1);
1211                    }
1212                    _ => panic!("Expected function"),
1213                }
1214            }
1215            _ => panic!("Expected SELECT query"),
1216        }
1217    }
1218
1219    #[test]
1220    fn test_select_distinct() {
1221        let query = parse("SELECT DISTINCT account").unwrap();
1222        match query {
1223            Query::Select(sel) => {
1224                assert!(sel.distinct);
1225            }
1226            _ => panic!("Expected SELECT query"),
1227        }
1228    }
1229
1230    #[test]
1231    fn test_select_distinct_no_space() {
1232        // Issue #640: DISTINCT(expr) without space should not be parsed as a function call
1233        let query = parse("SELECT DISTINCT(account) FROM postings").unwrap();
1234        match query {
1235            Query::Select(sel) => {
1236                assert!(sel.distinct);
1237            }
1238            _ => panic!("Expected SELECT query"),
1239        }
1240    }
1241
1242    #[test]
1243    fn test_select_distinct_coalesce_no_space() {
1244        // Issue #640: DISTINCT(COALESCE(payee, narration)) should work
1245        let query = parse("SELECT DISTINCT(COALESCE(payee, narration)) as payee FROM transactions")
1246            .unwrap();
1247        match query {
1248            Query::Select(sel) => {
1249                assert!(sel.distinct);
1250            }
1251            _ => panic!("Expected SELECT query"),
1252        }
1253    }
1254
1255    #[test]
1256    fn test_where_clause() {
1257        let query = parse("SELECT * WHERE account ~ \"Expenses:\"").unwrap();
1258        match query {
1259            Query::Select(sel) => {
1260                assert!(sel.where_clause.is_some());
1261                match sel.where_clause.unwrap() {
1262                    Expr::BinaryOp(op) => {
1263                        assert_eq!(op.op, BinaryOperator::Regex);
1264                    }
1265                    _ => panic!("Expected binary op"),
1266                }
1267            }
1268            _ => panic!("Expected SELECT query"),
1269        }
1270    }
1271
1272    #[test]
1273    fn test_group_by() {
1274        let query = parse("SELECT account, SUM(position) GROUP BY account").unwrap();
1275        match query {
1276            Query::Select(sel) => {
1277                assert!(sel.group_by.is_some());
1278                assert_eq!(sel.group_by.unwrap().len(), 1);
1279            }
1280            _ => panic!("Expected SELECT query"),
1281        }
1282    }
1283
1284    #[test]
1285    fn test_order_by() {
1286        let query = parse("SELECT * ORDER BY date DESC, account ASC").unwrap();
1287        match query {
1288            Query::Select(sel) => {
1289                assert!(sel.order_by.is_some());
1290                let order = sel.order_by.unwrap();
1291                assert_eq!(order.len(), 2);
1292                assert_eq!(order[0].direction, SortDirection::Desc);
1293                assert_eq!(order[1].direction, SortDirection::Asc);
1294            }
1295            _ => panic!("Expected SELECT query"),
1296        }
1297    }
1298
1299    #[test]
1300    fn test_limit() {
1301        let query = parse("SELECT * LIMIT 100").unwrap();
1302        match query {
1303            Query::Select(sel) => {
1304                assert_eq!(sel.limit, Some(100));
1305            }
1306            _ => panic!("Expected SELECT query"),
1307        }
1308    }
1309
1310    #[test]
1311    fn test_from_open_close_clear() {
1312        let query = parse("SELECT * FROM OPEN ON 2024-01-01 CLOSE ON 2024-12-31 CLEAR").unwrap();
1313        match query {
1314            Query::Select(sel) => {
1315                let from = sel.from.unwrap();
1316                assert_eq!(
1317                    from.open_on,
1318                    Some(rustledger_core::naive_date(2024, 1, 1).unwrap())
1319                );
1320                assert_eq!(
1321                    from.close_on,
1322                    Some(rustledger_core::naive_date(2024, 12, 31).unwrap())
1323                );
1324                assert!(from.clear);
1325            }
1326            _ => panic!("Expected SELECT query"),
1327        }
1328    }
1329
1330    #[test]
1331    fn test_from_modifier_followed_by_clause_parses() {
1332        // Regression: a FROM modifier (OPEN ON / CLOSE ON / CLEAR) followed by
1333        // another clause used to be a syntax error because the modifier consumed
1334        // the boundary whitespace the clause's `ws1()` needs.
1335        match parse("SELECT account FROM CLOSE ON 2021-01-01 ORDER BY account")
1336            .expect("FROM CLOSE ON <date> ORDER BY should parse")
1337        {
1338            Query::Select(sel) => {
1339                assert_eq!(
1340                    sel.from.unwrap().close_on,
1341                    Some(rustledger_core::naive_date(2021, 1, 1).unwrap())
1342                );
1343                assert!(
1344                    sel.order_by.is_some(),
1345                    "ORDER BY should be a clause, not consumed by the FROM filter"
1346                );
1347            }
1348            _ => panic!("Expected SELECT query"),
1349        }
1350        // WHERE / GROUP BY after a FROM modifier also parse.
1351        assert!(matches!(
1352            parse("SELECT account FROM OPEN ON 2020-06-01 WHERE account ~ \"Exp\""),
1353            Ok(Query::Select(_))
1354        ));
1355        assert!(matches!(
1356            parse("SELECT account FROM CLOSE ON 2021-01-01 GROUP BY account"),
1357            Ok(Query::Select(_))
1358        ));
1359        // A genuine FROM filter (no clause keyword) still parses as the filter.
1360        match parse("SELECT account FROM account ~ \"Exp\"").expect("FROM filter parses") {
1361            Query::Select(sel) => assert!(
1362                sel.from.unwrap().filter.is_some(),
1363                "a bare FROM expression should be the filter"
1364            ),
1365            _ => panic!("Expected SELECT query"),
1366        }
1367    }
1368
1369    #[test]
1370    fn test_from_year_filter() {
1371        let query = parse("SELECT date, account FROM year = 2024").unwrap();
1372        match query {
1373            Query::Select(sel) => {
1374                let from = sel.from.unwrap();
1375                assert!(from.filter.is_some(), "FROM filter should be present");
1376                match from.filter.unwrap() {
1377                    Expr::BinaryOp(op) => {
1378                        assert_eq!(op.op, BinaryOperator::Eq);
1379                        assert!(matches!(op.left, Expr::Column(ref c) if c == "year"));
1380                        // Right side can be Integer or Number (parser produces Number)
1381                        match op.right {
1382                            Expr::Literal(Literal::Integer(n)) => assert_eq!(n, 2024),
1383                            Expr::Literal(Literal::Number(n)) => assert_eq!(n, dec!(2024)),
1384                            other => panic!("Expected numeric literal, got {other:?}"),
1385                        }
1386                    }
1387                    other => panic!("Expected BinaryOp, got {other:?}"),
1388                }
1389            }
1390            _ => panic!("Expected SELECT query"),
1391        }
1392    }
1393
1394    #[test]
1395    fn test_journal_query() {
1396        let query = parse("JOURNAL \"Assets:Bank\" AT cost").unwrap();
1397        match query {
1398            Query::Journal(j) => {
1399                assert_eq!(j.account_pattern, "Assets:Bank");
1400                assert_eq!(j.at_function, Some("cost".to_string()));
1401            }
1402            _ => panic!("Expected JOURNAL query"),
1403        }
1404    }
1405
1406    #[test]
1407    fn test_balances_query() {
1408        let query = parse("BALANCES AT units FROM year = 2024").unwrap();
1409        match query {
1410            Query::Balances(b) => {
1411                assert_eq!(b.at_function, Some("units".to_string()));
1412                assert!(b.from.is_some());
1413            }
1414            _ => panic!("Expected BALANCES query"),
1415        }
1416    }
1417
1418    #[test]
1419    fn test_print_query() {
1420        let query = parse("PRINT").unwrap();
1421        assert!(matches!(query, Query::Print(_)));
1422    }
1423
1424    #[test]
1425    fn test_complex_expression() {
1426        let query = parse("SELECT * WHERE date >= 2024-01-01 AND account ~ \"Expenses:\"").unwrap();
1427        match query {
1428            Query::Select(sel) => match sel.where_clause.unwrap() {
1429                Expr::BinaryOp(op) => {
1430                    assert_eq!(op.op, BinaryOperator::And);
1431                }
1432                _ => panic!("Expected AND"),
1433            },
1434            _ => panic!("Expected SELECT query"),
1435        }
1436    }
1437
1438    #[test]
1439    fn test_integer_literal_parsing() {
1440        let query = parse("SELECT * WHERE year = 2024").unwrap();
1441        match query {
1442            Query::Select(sel) => match sel.where_clause.unwrap() {
1443                Expr::BinaryOp(op) => match op.right {
1444                    Expr::Literal(Literal::Integer(n)) => {
1445                        assert_eq!(n, 2024);
1446                    }
1447                    _ => panic!("Expected integer literal"),
1448                },
1449                _ => panic!("Expected binary op"),
1450            },
1451            _ => panic!("Expected SELECT query"),
1452        }
1453    }
1454
1455    #[test]
1456    fn test_integer_vs_decimal_literal() {
1457        // Whole-number literal → Integer
1458        let q = parse("SELECT * WHERE x = 42").unwrap();
1459        let Query::Select(sel) = q else {
1460            panic!("expected SELECT");
1461        };
1462        let Expr::BinaryOp(op) = sel.where_clause.unwrap() else {
1463            panic!("expected binary op");
1464        };
1465        assert!(matches!(op.right, Expr::Literal(Literal::Integer(42))));
1466
1467        // Literal with decimal point → Number, even when whole-valued
1468        let q = parse("SELECT * WHERE x = 42.0").unwrap();
1469        let Query::Select(sel) = q else {
1470            panic!("expected SELECT");
1471        };
1472        let Expr::BinaryOp(op) = sel.where_clause.unwrap() else {
1473            panic!("expected binary op");
1474        };
1475        match op.right {
1476            Expr::Literal(Literal::Number(n)) => assert_eq!(n, dec!(42.0)),
1477            other => panic!("expected Number literal, got {other:?}"),
1478        }
1479    }
1480
1481    #[test]
1482    fn test_integer_overflow_falls_back_to_number() {
1483        // i64::MAX is 9_223_372_036_854_775_807 — this exceeds it
1484        let q = parse("SELECT * WHERE x = 99999999999999999999").unwrap();
1485        let Query::Select(sel) = q else {
1486            panic!("expected SELECT");
1487        };
1488        let Expr::BinaryOp(op) = sel.where_clause.unwrap() else {
1489            panic!("expected binary op");
1490        };
1491        assert!(matches!(op.right, Expr::Literal(Literal::Number(_))));
1492    }
1493
1494    #[test]
1495    fn test_negative_integer_literal() {
1496        // Negative integer literals: the expression-level unary-minus rule
1497        // strips the `-` before the literal parser runs, so `-42` becomes
1498        // `Unary(Neg, Integer(42))` rather than `Integer(-42)`. Both forms
1499        // evaluate to `Value::Integer(-42)`.
1500        let q = parse("SELECT * WHERE x = -42").unwrap();
1501        let Query::Select(sel) = q else {
1502            panic!("expected SELECT");
1503        };
1504        let Expr::BinaryOp(op) = sel.where_clause.unwrap() else {
1505            panic!("expected binary op");
1506        };
1507        match op.right {
1508            Expr::UnaryOp(unary) => {
1509                assert_eq!(unary.op, UnaryOperator::Neg);
1510                assert!(matches!(unary.operand, Expr::Literal(Literal::Integer(42))));
1511            }
1512            other => panic!("expected Unary(Neg, Integer(42)), got {other:?}"),
1513        }
1514    }
1515
1516    #[test]
1517    fn test_semicolon_optional() {
1518        assert!(parse("SELECT *").is_ok());
1519        assert!(parse("SELECT *;").is_ok());
1520    }
1521
1522    #[test]
1523    fn test_subquery_basic() {
1524        let query = parse("SELECT * FROM (SELECT account, position)").unwrap();
1525        match query {
1526            Query::Select(sel) => {
1527                assert!(sel.from.is_some());
1528                let from = sel.from.unwrap();
1529                assert!(from.subquery.is_some());
1530                let subquery = from.subquery.unwrap();
1531                assert_eq!(subquery.targets.len(), 2);
1532            }
1533            _ => panic!("Expected SELECT query"),
1534        }
1535    }
1536
1537    #[test]
1538    fn test_subquery_with_groupby() {
1539        let query = parse(
1540            "SELECT account, total FROM (SELECT account, SUM(position) AS total GROUP BY account)",
1541        )
1542        .unwrap();
1543        match query {
1544            Query::Select(sel) => {
1545                assert_eq!(sel.targets.len(), 2);
1546                let from = sel.from.unwrap();
1547                assert!(from.subquery.is_some());
1548                let subquery = from.subquery.unwrap();
1549                assert!(subquery.group_by.is_some());
1550            }
1551            _ => panic!("Expected SELECT query"),
1552        }
1553    }
1554
1555    #[test]
1556    fn test_subquery_with_outer_where() {
1557        let query =
1558            parse("SELECT * FROM (SELECT * WHERE year = 2024) WHERE account ~ \"Expenses:\"")
1559                .unwrap();
1560        match query {
1561            Query::Select(sel) => {
1562                // Outer WHERE
1563                assert!(sel.where_clause.is_some());
1564                // Subquery with its own WHERE
1565                let from = sel.from.unwrap();
1566                let subquery = from.subquery.unwrap();
1567                assert!(subquery.where_clause.is_some());
1568            }
1569            _ => panic!("Expected SELECT query"),
1570        }
1571    }
1572
1573    #[test]
1574    fn test_nested_subquery() {
1575        // Two levels of nesting
1576        let query = parse("SELECT * FROM (SELECT * FROM (SELECT account))").unwrap();
1577        match query {
1578            Query::Select(sel) => {
1579                let from = sel.from.unwrap();
1580                let subquery1 = from.subquery.unwrap();
1581                let from2 = subquery1.from.unwrap();
1582                assert!(from2.subquery.is_some());
1583            }
1584            _ => panic!("Expected SELECT query"),
1585        }
1586    }
1587
1588    #[test]
1589    fn test_nested_function_calls() {
1590        // Test units(sum(position)) pattern
1591        let query = parse("SELECT units(sum(position))").unwrap();
1592        match query {
1593            Query::Select(sel) => {
1594                assert_eq!(sel.targets.len(), 1);
1595                match &sel.targets[0].expr {
1596                    Expr::Function(outer) => {
1597                        assert_eq!(outer.name, "units");
1598                        assert_eq!(outer.args.len(), 1);
1599                        match &outer.args[0] {
1600                            Expr::Function(inner) => {
1601                                assert_eq!(inner.name, "sum");
1602                                assert_eq!(inner.args.len(), 1);
1603                                assert!(
1604                                    matches!(&inner.args[0], Expr::Column(c) if c == "position")
1605                                );
1606                            }
1607                            _ => panic!("Expected inner function call"),
1608                        }
1609                    }
1610                    _ => panic!("Expected outer function call"),
1611                }
1612            }
1613            _ => panic!("Expected SELECT query"),
1614        }
1615    }
1616
1617    #[test]
1618    fn test_deeply_nested_function_calls() {
1619        // Test three levels of nesting
1620        let query = parse("SELECT foo(bar(baz(x)))").unwrap();
1621        match query {
1622            Query::Select(sel) => {
1623                assert_eq!(sel.targets.len(), 1);
1624                match &sel.targets[0].expr {
1625                    Expr::Function(f1) => {
1626                        assert_eq!(f1.name, "foo");
1627                        match &f1.args[0] {
1628                            Expr::Function(f2) => {
1629                                assert_eq!(f2.name, "bar");
1630                                match &f2.args[0] {
1631                                    Expr::Function(f3) => {
1632                                        assert_eq!(f3.name, "baz");
1633                                        assert!(matches!(&f3.args[0], Expr::Column(c) if c == "x"));
1634                                    }
1635                                    _ => panic!("Expected f3"),
1636                                }
1637                            }
1638                            _ => panic!("Expected f2"),
1639                        }
1640                    }
1641                    _ => panic!("Expected f1"),
1642                }
1643            }
1644            _ => panic!("Expected SELECT query"),
1645        }
1646    }
1647
1648    #[test]
1649    fn test_function_with_arithmetic() {
1650        // Test function with arithmetic expression as argument
1651        let query = parse("SELECT sum(amount * 2)").unwrap();
1652        match query {
1653            Query::Select(sel) => match &sel.targets[0].expr {
1654                Expr::Function(f) => {
1655                    assert_eq!(f.name, "sum");
1656                    assert!(matches!(&f.args[0], Expr::BinaryOp(_)));
1657                }
1658                _ => panic!("Expected function"),
1659            },
1660            _ => panic!("Expected SELECT query"),
1661        }
1662    }
1663
1664    #[test]
1665    fn test_is_null() {
1666        let query = parse("SELECT * WHERE payee IS NULL").unwrap();
1667        match query {
1668            Query::Select(sel) => match sel.where_clause.unwrap() {
1669                Expr::UnaryOp(op) => {
1670                    assert_eq!(op.op, UnaryOperator::IsNull);
1671                    assert!(matches!(&op.operand, Expr::Column(c) if c == "payee"));
1672                }
1673                _ => panic!("Expected unary op"),
1674            },
1675            _ => panic!("Expected SELECT query"),
1676        }
1677    }
1678
1679    #[test]
1680    fn test_is_not_null() {
1681        let query = parse("SELECT * WHERE payee IS NOT NULL").unwrap();
1682        match query {
1683            Query::Select(sel) => match sel.where_clause.unwrap() {
1684                Expr::UnaryOp(op) => {
1685                    assert_eq!(op.op, UnaryOperator::IsNotNull);
1686                    assert!(matches!(&op.operand, Expr::Column(c) if c == "payee"));
1687                }
1688                _ => panic!("Expected unary op"),
1689            },
1690            _ => panic!("Expected SELECT query"),
1691        }
1692    }
1693
1694    #[test]
1695    fn test_not_regex() {
1696        let query = parse("SELECT * WHERE account !~ \"Assets:\"").unwrap();
1697        match query {
1698            Query::Select(sel) => match sel.where_clause.unwrap() {
1699                Expr::BinaryOp(op) => {
1700                    assert_eq!(op.op, BinaryOperator::NotRegex);
1701                }
1702                _ => panic!("Expected binary op"),
1703            },
1704            _ => panic!("Expected SELECT query"),
1705        }
1706    }
1707
1708    #[test]
1709    fn test_modulo() {
1710        let query = parse("SELECT year % 4").unwrap();
1711        match query {
1712            Query::Select(sel) => match &sel.targets[0].expr {
1713                Expr::BinaryOp(op) => {
1714                    assert_eq!(op.op, BinaryOperator::Mod);
1715                }
1716                _ => panic!("Expected binary op"),
1717            },
1718            _ => panic!("Expected SELECT query"),
1719        }
1720    }
1721
1722    #[test]
1723    fn test_between() {
1724        let query = parse("SELECT * WHERE year BETWEEN 2020 AND 2024").unwrap();
1725        match query {
1726            Query::Select(sel) => match sel.where_clause.unwrap() {
1727                Expr::Between { value, low, high } => {
1728                    assert!(matches!(*value, Expr::Column(c) if c == "year"));
1729                    assert!(matches!(*low, Expr::Literal(Literal::Integer(_))));
1730                    assert!(matches!(*high, Expr::Literal(Literal::Integer(_))));
1731                }
1732                _ => panic!("Expected BETWEEN"),
1733            },
1734            _ => panic!("Expected SELECT query"),
1735        }
1736    }
1737
1738    #[test]
1739    fn test_not_in() {
1740        let query = parse("SELECT * WHERE account NOT IN tags").unwrap();
1741        match query {
1742            Query::Select(sel) => match sel.where_clause.unwrap() {
1743                Expr::BinaryOp(op) => {
1744                    assert_eq!(op.op, BinaryOperator::NotIn);
1745                }
1746                _ => panic!("Expected binary op"),
1747            },
1748            _ => panic!("Expected SELECT query"),
1749        }
1750    }
1751
1752    #[test]
1753    fn test_in_set_literal() {
1754        // Multi-element set literal
1755        let query = parse("SELECT * WHERE currency IN ('EUR', 'USD')").unwrap();
1756        match query {
1757            Query::Select(sel) => match sel.where_clause.unwrap() {
1758                Expr::BinaryOp(op) => {
1759                    assert_eq!(op.op, BinaryOperator::In);
1760                    match op.right {
1761                        Expr::Set(elements) => {
1762                            assert_eq!(elements.len(), 2);
1763                        }
1764                        _ => panic!("Expected Set"),
1765                    }
1766                }
1767                _ => panic!("Expected binary op"),
1768            },
1769            _ => panic!("Expected SELECT query"),
1770        }
1771
1772        // Single-element set with trailing comma
1773        let query = parse("SELECT * WHERE currency IN ('EUR',)").unwrap();
1774        match query {
1775            Query::Select(sel) => match sel.where_clause.unwrap() {
1776                Expr::BinaryOp(op) => {
1777                    assert_eq!(op.op, BinaryOperator::In);
1778                    match op.right {
1779                        Expr::Set(elements) => {
1780                            assert_eq!(elements.len(), 1);
1781                        }
1782                        _ => panic!("Expected Set"),
1783                    }
1784                }
1785                _ => panic!("Expected binary op"),
1786            },
1787            _ => panic!("Expected SELECT query"),
1788        }
1789
1790        // Parenthesized column (not a set literal)
1791        let query = parse("SELECT * WHERE 'x' IN (tags)").unwrap();
1792        match query {
1793            Query::Select(sel) => match sel.where_clause.unwrap() {
1794                Expr::BinaryOp(op) => {
1795                    assert_eq!(op.op, BinaryOperator::In);
1796                    // Should be Paren(Column), not Set([Column])
1797                    match op.right {
1798                        Expr::Paren(inner) => match *inner {
1799                            Expr::Column(name) => assert_eq!(name, "tags"),
1800                            _ => panic!("Expected Column inside Paren"),
1801                        },
1802                        other => panic!("Expected Paren, got {other:?}"),
1803                    }
1804                }
1805                _ => panic!("Expected binary op"),
1806            },
1807            _ => panic!("Expected SELECT query"),
1808        }
1809    }
1810
1811    #[test]
1812    fn test_string_arg_function() {
1813        // First test a function with a column reference - should work
1814        let query = parse("SELECT foo(x)").unwrap();
1815        match query {
1816            Query::Select(sel) => match &sel.targets[0].expr {
1817                Expr::Function(f) => {
1818                    assert_eq!(f.name, "foo");
1819                }
1820                _ => panic!("Expected function"),
1821            },
1822            _ => panic!("Expected SELECT query"),
1823        }
1824
1825        // Now test a function with a string literal argument
1826        let query = parse("SELECT foo('bar')").unwrap();
1827        match query {
1828            Query::Select(sel) => match &sel.targets[0].expr {
1829                Expr::Function(f) => {
1830                    assert_eq!(f.name, "foo");
1831                    assert!(matches!(&f.args[0], Expr::Literal(Literal::String(s)) if s == "bar"));
1832                }
1833                _ => panic!("Expected function"),
1834            },
1835            _ => panic!("Expected SELECT query"),
1836        }
1837    }
1838
1839    #[test]
1840    fn test_meta_function() {
1841        let query = parse("SELECT meta('category')").unwrap();
1842        match query {
1843            Query::Select(sel) => match &sel.targets[0].expr {
1844                Expr::Function(f) => {
1845                    assert_eq!(f.name.to_uppercase(), "META");
1846                    assert_eq!(f.args.len(), 1);
1847                    assert!(
1848                        matches!(&f.args[0], Expr::Literal(Literal::String(s)) if s == "category")
1849                    );
1850                }
1851                _ => panic!("Expected function"),
1852            },
1853            _ => panic!("Expected SELECT query"),
1854        }
1855    }
1856
1857    #[test]
1858    fn test_entry_meta_function() {
1859        let query = parse("SELECT entry_meta('source')").unwrap();
1860        match query {
1861            Query::Select(sel) => match &sel.targets[0].expr {
1862                Expr::Function(f) => {
1863                    assert_eq!(f.name.to_uppercase(), "ENTRY_META");
1864                    assert_eq!(f.args.len(), 1);
1865                }
1866                _ => panic!("Expected function"),
1867            },
1868            _ => panic!("Expected SELECT query"),
1869        }
1870    }
1871
1872    #[test]
1873    fn test_convert_function() {
1874        let query = parse("SELECT convert(position, 'USD')").unwrap();
1875        match query {
1876            Query::Select(sel) => match &sel.targets[0].expr {
1877                Expr::Function(f) => {
1878                    assert_eq!(f.name.to_uppercase(), "CONVERT");
1879                    assert_eq!(f.args.len(), 2);
1880                }
1881                _ => panic!("Expected function"),
1882            },
1883            _ => panic!("Expected SELECT query"),
1884        }
1885    }
1886
1887    #[test]
1888    fn test_type_cast_functions() {
1889        // Test INT
1890        let query = parse("SELECT int(number)").unwrap();
1891        match query {
1892            Query::Select(sel) => match &sel.targets[0].expr {
1893                Expr::Function(f) => {
1894                    assert_eq!(f.name.to_uppercase(), "INT");
1895                    assert_eq!(f.args.len(), 1);
1896                }
1897                _ => panic!("Expected function"),
1898            },
1899            _ => panic!("Expected SELECT query"),
1900        }
1901
1902        // Test DECIMAL
1903        let query = parse("SELECT decimal('123.45')").unwrap();
1904        match query {
1905            Query::Select(sel) => match &sel.targets[0].expr {
1906                Expr::Function(f) => {
1907                    assert_eq!(f.name.to_uppercase(), "DECIMAL");
1908                }
1909                _ => panic!("Expected function"),
1910            },
1911            _ => panic!("Expected SELECT query"),
1912        }
1913
1914        // Test STR
1915        let query = parse("SELECT str(123)").unwrap();
1916        match query {
1917            Query::Select(sel) => match &sel.targets[0].expr {
1918                Expr::Function(f) => {
1919                    assert_eq!(f.name.to_uppercase(), "STR");
1920                }
1921                _ => panic!("Expected function"),
1922            },
1923            _ => panic!("Expected SELECT query"),
1924        }
1925
1926        // Test BOOL
1927        let query = parse("SELECT bool(1)").unwrap();
1928        match query {
1929            Query::Select(sel) => match &sel.targets[0].expr {
1930                Expr::Function(f) => {
1931                    assert_eq!(f.name.to_uppercase(), "BOOL");
1932                }
1933                _ => panic!("Expected function"),
1934            },
1935            _ => panic!("Expected SELECT query"),
1936        }
1937    }
1938
1939    #[test]
1940    fn test_system_table_prices() {
1941        // Test parsing SELECT FROM #prices (system table)
1942        let query = parse("SELECT date, currency, amount FROM #prices").unwrap();
1943        match query {
1944            Query::Select(sel) => {
1945                assert_eq!(sel.targets.len(), 3);
1946                assert!(matches!(&sel.targets[0].expr, Expr::Column(c) if c == "date"));
1947                assert!(matches!(&sel.targets[1].expr, Expr::Column(c) if c == "currency"));
1948                assert!(matches!(&sel.targets[2].expr, Expr::Column(c) if c == "amount"));
1949                let from = sel.from.unwrap();
1950                assert_eq!(from.table_name, Some("#prices".to_string()));
1951            }
1952            _ => panic!("Expected SELECT query"),
1953        }
1954    }
1955
1956    #[test]
1957    fn test_system_table_with_where() {
1958        // Test parsing system table with WHERE clause
1959        let query = parse("SELECT * FROM #prices WHERE currency = 'EUR'").unwrap();
1960        match query {
1961            Query::Select(sel) => {
1962                let from = sel.from.unwrap();
1963                assert_eq!(from.table_name, Some("#prices".to_string()));
1964                assert!(sel.where_clause.is_some());
1965            }
1966            _ => panic!("Expected SELECT query"),
1967        }
1968    }
1969
1970    #[test]
1971    fn test_regular_table_identifier() {
1972        // Test parsing a regular (non-system) table
1973        let query = parse("SELECT * FROM MyTable WHERE x = 1").unwrap();
1974        match query {
1975            Query::Select(sel) => {
1976                let from = sel.from.unwrap();
1977                assert_eq!(from.table_name, Some("MyTable".to_string()));
1978            }
1979            _ => panic!("Expected SELECT query"),
1980        }
1981    }
1982
1983    /// Pathologically deep paren nesting must fail fast with a clean
1984    /// error rather than overflowing the stack or parsing in
1985    /// super-linear time (query-fuzzer slow-unit denial-of-service).
1986    #[test]
1987    fn deeply_nested_parens_rejected_without_stack_overflow() {
1988        let src = format!("SELECT {}", "(".repeat(2000));
1989        let err = parse(&src).expect_err("deeply nested parens should be rejected");
1990        assert!(
1991            matches!(err.kind, ParseErrorKind::SyntaxError(ref m) if m.contains("nesting too deep")),
1992            "expected a nesting-depth error, got: {:?}",
1993            err.kind
1994        );
1995    }
1996
1997    /// The guard must not reject legitimately nested queries: nesting up
1998    /// to the limit still parses.
1999    #[test]
2000    fn moderate_nesting_still_parses() {
2001        // 100 levels of real function nesting (well under the 128 cap).
2002        // Runs on `parse`'s dedicated big-stack thread, so this passes
2003        // regardless of the 2 MiB cargo-test thread stack or toolchain
2004        // frame-size drift (Rust 1.97 SIGABRT'd this test at the old
2005        // caller-stack recursion).
2006        let depth = 100usize;
2007        let inner = "x".to_string();
2008        let body = format!("{}{}{}", "abs(".repeat(depth), inner, ")".repeat(depth));
2009        let src = format!("SELECT {body}");
2010        assert!(
2011            parse(&src).is_ok(),
2012            "a {depth}-deep nested query within the limit should parse"
2013        );
2014    }
2015
2016    /// Parens inside string literals are opaque and must not count
2017    /// toward the nesting limit.
2018    #[test]
2019    fn parens_inside_string_literal_dont_count() {
2020        let src = format!("SELECT account WHERE narration = \"{}\"", "(".repeat(2000));
2021        // Assert directly on the guard: 2000 parens inside a string
2022        // literal must not register as nesting depth at all.
2023        assert!(
2024            nesting_exceeds_limit(&src).is_none(),
2025            "parens inside a string literal must not count toward the nesting limit"
2026        );
2027        // And the query as a whole still parses (the string is opaque).
2028        assert!(parse(&src).is_ok(), "string-literal query should parse");
2029    }
2030}