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