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