qail_core/
parser.rs

1//! QAIL Parser using nom.
2//!
3//! Parses QAIL syntax into an AST.
4//!
5//! # Syntax Overview (v2.0 - Rust Native)
6//!
7//! ```text
8//! get::users:'id'email [ 'active == true, -created_at, 0..10 ]
9//! ─┬─ ─┬─ ─┬────┬──── ─────────────────┬────────────────────
10//!  │   │   │    │                      │
11//!  │   │   │    │                      └── Unified Block (filters, sorts, ranges)
12//!  │   │   │    └── Labels (columns with ')
13//!  │   │   └── Link (connects to table with :)
14//!  │   └── Table name
15//!  └── Gate (action with ::)
16//! ```
17//!
18//! ## Syntax v2.0 Reference
19//!
20//! | Symbol | Name   | Function          | Example |
21//! |--------|--------|-------------------|---------|
22//! | `::`   | Gate   | Action            | `get::users` |
23//! | `:`    | Link   | Table to columns  | `get::users:'id` |
24//! | `'`    | Label  | Column marker     | `'email'name` |
25//! | `'_`   | Infer  | All columns       | `'_` → `*` |
26//! | `==`   | Equal  | Filter            | `'active == true` |
27//! | `~`    | Match  | Fuzzy/ILIKE       | `'name ~ "john"` |
28//! | `-`    | Desc   | Sort descending   | `-created_at` |
29//! | `+`    | Asc    | Sort ascending    | `+id` |
30//! | `..`   | Range  | Limit/Offset      | `0..10` = LIMIT 10 OFFSET 0 |
31//! | `->`   | Inner  | Inner Join        | `users -> posts` |
32//! | `<-`   | Left   | Left Join         | `users <- posts` |
33//! | `->>`  | Right  | Right Join        | `orders ->> customers` |
34//! | `$`    | Param  | Bind variable     | `$1` |
35
36use nom::{
37    branch::alt,
38    bytes::complete::{tag, take_while, take_while1},
39    character::complete::{char, digit1, multispace1, not_line_ending},
40    combinator::{map, opt, recognize, value},
41    multi::many0,
42    sequence::{pair, preceded, tuple},
43    IResult,
44};
45
46use crate::ast::*;
47use crate::error::{QailError, QailResult};
48
49/// Parse whitespace or comments.
50fn ws_or_comment(input: &str) -> IResult<&str, ()> {
51    value((), many0(alt((
52        value((), multispace1),
53        parse_comment,
54    ))))(input)
55}
56
57/// Parse a single comment line (// ... or -- ...).
58fn parse_comment(input: &str) -> IResult<&str, ()> {
59    value((), pair(alt((tag("//"), tag("--"))), not_line_ending))(input)
60}
61
62/// Parse a complete QAIL query string.
63pub fn parse(input: &str) -> QailResult<QailCmd> {
64    let input = input.trim();
65    
66    match parse_qail_cmd(input) {
67        Ok(("", cmd)) => Ok(cmd),
68        Ok((remaining, _)) => Err(QailError::parse(
69            input.len() - remaining.len(),
70            format!("Unexpected trailing content: '{}'", remaining),
71        )),
72        Err(e) => Err(QailError::parse(0, format!("Parse failed: {:?}", e))),
73    }
74}
75
76/// Parse the complete QAIL command.
77fn parse_qail_cmd(input: &str) -> IResult<&str, QailCmd> {
78    let (input, action) = parse_action(input)?;
79    // Check for ! after action for DISTINCT (e.g., get!::users)
80    let (input, distinct_marker) = opt(char('!'))(input)?;
81    let distinct = distinct_marker.is_some();
82    let (input, _) = tag("::")(input)?;
83    let (input, table) = parse_identifier(input)?;
84    let (input, joins) = parse_joins(input)?;
85    let (input, _) = ws_or_comment(input)?;
86    // Link character ':' is optional - connects table to columns
87    let (input, _) = opt(char(':'))(input)?;
88    let (input, _) = ws_or_comment(input)?;
89    let (input, columns) = parse_columns(input)?;
90    let (input, _) = ws_or_comment(input)?;
91    let (input, cages) = parse_unified_blocks(input)?;
92
93    Ok((
94        input,
95        QailCmd {
96            action,
97            table: table.to_string(),
98            joins,
99            columns,
100            cages,
101            distinct,
102        },
103    ))
104}
105
106/// Parse the action (get, set, del, add, gen, make, mod, over, with).
107fn parse_action(input: &str) -> IResult<&str, Action> {
108    alt((
109        value(Action::Get, tag("get")),
110        value(Action::Set, tag("set")),
111        value(Action::Del, tag("del")),
112        value(Action::Add, tag("add")),
113        value(Action::Gen, tag("gen")),
114        value(Action::Make, tag("make")),
115        value(Action::Mod, tag("mod")),
116        value(Action::Over, tag("over")),
117        value(Action::With, tag("with")),
118    ))(input)
119}
120
121/// Parse an identifier (table name, column name).
122fn parse_identifier(input: &str) -> IResult<&str, &str> {
123    take_while1(|c: char| c.is_alphanumeric() || c == '_')(input)
124}
125
126fn parse_joins(input: &str) -> IResult<&str, Vec<Join>> {
127    many0(parse_single_join)(input)
128}
129
130/// Parse a single join: `->` (INNER), `<-` (LEFT), `->>` (RIGHT)
131fn parse_single_join(input: &str) -> IResult<&str, Join> {
132    let (input, _) = ws_or_comment(input)?;
133    
134    // Try RIGHT JOIN first (->>)
135    if let Ok((remaining, _)) = tag::<_, _, nom::error::Error<&str>>("->>") (input) {
136        let (remaining, _) = ws_or_comment(remaining)?;
137        let (remaining, table) = parse_identifier(remaining)?;
138        return Ok((remaining, Join {
139            table: table.to_string(),
140            kind: JoinKind::Right,
141        }));
142    }
143    
144    // Try LEFT JOIN (<-)
145    if let Ok((remaining, _)) = tag::<_, _, nom::error::Error<&str>>("<-") (input) {
146        let (remaining, _) = ws_or_comment(remaining)?;
147        let (remaining, table) = parse_identifier(remaining)?;
148        return Ok((remaining, Join {
149            table: table.to_string(),
150            kind: JoinKind::Left,
151        }));
152    }
153    
154    // Default: INNER JOIN (->)
155    let (input, _) = tag("->")(input)?;
156    let (input, _) = ws_or_comment(input)?;
157    let (input, table) = parse_identifier(input)?;
158    Ok((input, Join {
159        table: table.to_string(),
160        kind: JoinKind::Inner,
161    }))
162}
163
164/// Parse columns using the v2.0 label syntax ('col).
165fn parse_columns(input: &str) -> IResult<&str, Vec<Column>> {
166    many0(preceded(ws_or_comment, parse_any_column))(input)
167}
168
169fn parse_any_column(input: &str) -> IResult<&str, Column> {
170    alt((
171        // v2.0 Label: 'col...
172        preceded(char('\''), parse_label_column),
173        // Legacy Hook: @col... (for backwards compat during migration)
174        preceded(char('@'), parse_at_column),
175    ))(input)
176}
177
178/// Parse a column with the v2.0 label syntax ('col).
179fn parse_label_column(input: &str) -> IResult<&str, Column> {
180    alt((
181        // Wildcard: '_ for all columns
182        value(Column::Star, char('_')),
183        // Named or complex column
184        parse_column_full_def_or_named,
185    ))(input)
186}
187
188/// Legacy @ column parsing for backwards compatibility.
189fn parse_at_column(input: &str) -> IResult<&str, Column> {
190    alt((
191        value(Column::Star, char('*')),
192        // Check for drop via @-name convention
193        map(preceded(char('-'), parse_identifier), |name| Column::Mod { 
194            kind: ModKind::Drop, 
195            col: Box::new(Column::Named(name.to_string())) 
196        }),
197        parse_column_full_def_or_named, 
198    ))(input)
199}
200
201fn parse_column_full_def_or_named(input: &str) -> IResult<&str, Column> {
202    // 1. Parse Name
203    let (input, name) = parse_identifier(input)?;
204    
205    // 2. Opt: Aggregates (#func)
206    if let Ok((input, Some(func))) = opt(preceded(char('#'), parse_agg_func))(input) {
207        return Ok((input, Column::Aggregate {
208             col: name.to_string(),
209             func
210        }));
211    }
212    
213    // 3. Opt: check for colon (type definition)
214    if let Ok((input, _)) = char::<_, nom::error::Error<&str>>(':')(input) {
215        // We have a type OR a window function.
216        let (input, type_or_func) = parse_identifier(input)?;
217        
218        let (input, _) = ws_or_comment(input)?;
219        
220        // Peek/Check for open paren `(` for window function
221        if let Ok((input, _)) = char::<_, nom::error::Error<&str>>('(')(input) {
222            // It IS a function call -> Window Column
223            let (input, _) = ws_or_comment(input)?;
224            let (input, args) = opt(tuple((
225                parse_value,
226                many0(preceded(
227                    tuple((ws_or_comment, char(','), ws_or_comment)),
228                    parse_value
229                ))
230            )))(input)?;
231            let (input, _) = ws_or_comment(input)?;
232            let (input, _) = char(')')(input)?;
233            
234            let params = match args {
235                Some((first, mut rest)) => {
236                    let mut v = vec![first];
237                    v.append(&mut rest);
238                    v
239                },
240                None => vec![],
241            };
242
243            // Parse Order Cages (e.g. ^!amount) - legacy syntax
244            let (input, sorts) = many0(parse_legacy_sort_cage)(input)?;
245            
246            // Parse Partition: {Part=...}
247            let (input, partitions) = opt(parse_partition_block)(input)?;
248            let partition = partitions.unwrap_or_default();
249
250            return Ok((input, Column::Window {
251                name: name.to_string(),
252                func: type_or_func.to_string(),
253                params,
254                partition,
255                order: sorts,
256            }));
257        } else {
258            // It is just a Type Definition
259            let (input, constraints) = parse_constraints(input)?;
260            
261            return Ok((input, Column::Def { 
262                name: name.to_string(), 
263                data_type: type_or_func.to_string(), 
264                constraints 
265            }));
266        }
267    }
268    
269    // No colon, check for constraints (inferred type Def)
270    let (input, constraints) = parse_constraints(input)?;
271    if !constraints.is_empty() {
272         Ok((input, Column::Def { 
273            name: name.to_string(), 
274            data_type: "str".to_string(), 
275            constraints 
276        }))
277    } else {
278        // Just a named column
279        Ok((input, Column::Named(name.to_string())))
280    }
281}
282
283fn parse_constraints(input: &str) -> IResult<&str, Vec<Constraint>> {
284    many0(alt((
285        value(Constraint::PrimaryKey, tag("^pk")),
286        value(Constraint::Unique, tag("^uniq")),
287        value(Constraint::Nullable, char('?')),
288    )))(input)
289}
290
291fn parse_agg_func(input: &str) -> IResult<&str, AggregateFunc> {
292    alt((
293        value(AggregateFunc::Count, tag("count")),
294        value(AggregateFunc::Sum, tag("sum")),
295        value(AggregateFunc::Avg, tag("avg")),
296        value(AggregateFunc::Min, tag("min")),
297        value(AggregateFunc::Max, tag("max")),
298    ))(input)
299}
300
301/// Parse unified constraint blocks [...].
302/// v2.0 syntax: [ 'active == true, -created_at, 0..10 ]
303fn parse_unified_blocks(input: &str) -> IResult<&str, Vec<Cage>> {
304    many0(preceded(ws_or_comment, parse_unified_block))(input)
305}
306
307/// Parse a unified constraint block [...].
308/// Contains comma-separated items: filters, sorts, ranges.
309fn parse_unified_block(input: &str) -> IResult<&str, Cage> {
310    let (input, _) = char('[')(input)?;
311    let (input, _) = ws_or_comment(input)?;
312    
313    // Parse all items in the block (comma-separated)
314    let (input, items) = parse_block_items(input)?;
315    
316    let (input, _) = ws_or_comment(input)?;
317    let (input, _) = char(']')(input)?;
318    
319    // Convert items into appropriate cages
320    // For now, combine into a single cage or return the first meaningful one
321    items_to_cage(items, input)
322}
323
324/// Represents a parsed item within a unified block.
325#[derive(Debug)]
326enum BlockItem {
327    Filter(Condition, LogicalOp),
328    Sort(String, SortOrder),
329    Range(usize, Option<usize>), // start, end
330    LegacyLimit(usize),
331    LegacyOffset(usize),
332}
333
334/// Parse comma-separated items within a block.
335/// Also handles | (OR) and & (AND) operators for filter conditions.
336fn parse_block_items(input: &str) -> IResult<&str, Vec<BlockItem>> {
337    let (input, first) = opt(parse_block_item)(input)?;
338    
339    match first {
340        None => Ok((input, vec![])),
341        Some(mut item) => {
342            let mut items = vec![];
343            let mut remaining = input;
344            
345            loop {
346                let (input, _) = ws_or_comment(remaining)?;
347                
348                // Check for various separators: comma, pipe (OR), ampersand (AND)
349                if let Ok((input, _)) = char::<_, nom::error::Error<&str>>(',')(input) {
350                    // Comma separator - add current item and parse next
351                    items.push(item);
352                    let (input, _) = ws_or_comment(input)?;
353                    let (input, next_item) = parse_block_item(input)?;
354                    item = next_item;
355                    remaining = input;
356                } else if let Ok((new_input, _)) = char::<_, nom::error::Error<&str>>('|')(input) {
357                    // OR separator - update item's logical op and parse next filter
358                    if let BlockItem::Filter(cond, _) = item {
359                        items.push(BlockItem::Filter(cond, LogicalOp::Or));
360                    } else {
361                        items.push(item);
362                    }
363                    let (new_input, _) = ws_or_comment(new_input)?;
364                    let (new_input, next_item) = parse_filter_item(new_input)?;
365                    // Mark the next item as part of an OR chain
366                    if let BlockItem::Filter(cond, _) = next_item {
367                        item = BlockItem::Filter(cond, LogicalOp::Or);
368                    } else {
369                        item = next_item;
370                    }
371                    remaining = new_input;
372                } else if let Ok((new_input, _)) = char::<_, nom::error::Error<&str>>('&')(input) {
373                    // AND separator
374                    items.push(item);
375                    let (new_input, _) = ws_or_comment(new_input)?;
376                    let (new_input, next_item) = parse_filter_item(new_input)?;
377                    item = next_item;
378                    remaining = new_input;
379                } else {
380                    items.push(item);
381                    remaining = input;
382                    break;
383                }
384            }
385            
386            Ok((remaining, items))
387        }
388    }
389}
390
391/// Parse a single item in a unified block.
392fn parse_block_item(input: &str) -> IResult<&str, BlockItem> {
393    alt((
394        // Range: N..M or N.. (must try before other number parsing)
395        parse_range_item,
396        // Sort: +col (asc) or -col (desc)
397        parse_sort_item,
398        // Legacy limit: lim=N
399        parse_legacy_limit_item,
400        // Legacy offset: off=N
401        parse_legacy_offset_item,
402        // Legacy sort: ^col or ^!col
403        parse_legacy_sort_item,
404        // Filter: 'col == value
405        parse_filter_item,
406    ))(input)
407}
408
409/// Parse a range item: N..M or N..
410fn parse_range_item(input: &str) -> IResult<&str, BlockItem> {
411    let (input, start) = digit1(input)?;
412    let (input, _) = tag("..")(input)?;
413    let (input, end) = opt(digit1)(input)?;
414    
415    let start_num: usize = start.parse().unwrap_or(0);
416    let end_num = end.map(|e| e.parse().unwrap_or(0));
417    
418    Ok((input, BlockItem::Range(start_num, end_num)))
419}
420
421/// Parse a sort item: +col (asc) or -col (desc).
422fn parse_sort_item(input: &str) -> IResult<&str, BlockItem> {
423    alt((
424        map(preceded(char('+'), parse_identifier), |col| {
425            BlockItem::Sort(col.to_string(), SortOrder::Asc)
426        }),
427        map(preceded(char('-'), parse_identifier), |col| {
428            BlockItem::Sort(col.to_string(), SortOrder::Desc)
429        }),
430    ))(input)
431}
432
433/// Parse legacy limit: lim=N
434fn parse_legacy_limit_item(input: &str) -> IResult<&str, BlockItem> {
435    let (input, _) = tag("lim")(input)?;
436    let (input, _) = ws_or_comment(input)?;
437    let (input, _) = char('=')(input)?;
438    let (input, _) = ws_or_comment(input)?;
439    let (input, n) = digit1(input)?;
440    Ok((input, BlockItem::LegacyLimit(n.parse().unwrap_or(10))))
441}
442
443/// Parse legacy offset: off=N
444fn parse_legacy_offset_item(input: &str) -> IResult<&str, BlockItem> {
445    let (input, _) = tag("off")(input)?;
446    let (input, _) = ws_or_comment(input)?;
447    let (input, _) = char('=')(input)?;
448    let (input, _) = ws_or_comment(input)?;
449    let (input, n) = digit1(input)?;
450    Ok((input, BlockItem::LegacyOffset(n.parse().unwrap_or(0))))
451}
452
453/// Parse legacy sort: ^col or ^!col
454fn parse_legacy_sort_item(input: &str) -> IResult<&str, BlockItem> {
455    let (input, _) = char('^')(input)?;
456    let (input, desc) = opt(char('!'))(input)?;
457    let (input, col) = parse_identifier(input)?;
458    
459    let order = if desc.is_some() {
460        SortOrder::Desc
461    } else {
462        SortOrder::Asc
463    };
464    
465    Ok((input, BlockItem::Sort(col.to_string(), order)))
466}
467
468/// Parse a filter item: 'col == value or col == value
469fn parse_filter_item(input: &str) -> IResult<&str, BlockItem> {
470    // Optional leading ' for column (v2.0 style)
471    let (input, _) = opt(char('\''))(input)?;
472    let (input, column) = parse_identifier(input)?;
473    
474    // Check for array unnest syntax: column[*]
475    let (input, is_array_unnest) = if input.starts_with("[*]") {
476        (&input[3..], true)
477    } else {
478        (input, false)
479    };
480    
481    let (input, _) = ws_or_comment(input)?;
482    let (input, (op, val)) = parse_operator_and_value(input)?;
483    
484    Ok((input, BlockItem::Filter(
485        Condition {
486            column: column.to_string(),
487            op,
488            value: val,
489            is_array_unnest,
490        },
491        LogicalOp::And, // Default, could be enhanced for | and &
492    )))
493}
494
495/// Convert parsed block items into a Cage.
496fn items_to_cage(items: Vec<BlockItem>, input: &str) -> IResult<&str, Cage> {
497    // Default: return a filter cage if we have filters
498    let mut conditions = Vec::new();
499    let mut logical_op = LogicalOp::And;
500    
501    // Check for special single-item cases
502    for item in &items {
503        match item {
504            BlockItem::Range(start, end) => {
505                // Range: start..end means OFFSET start, LIMIT (end - start)
506                // If end is None, it's just OFFSET start
507                // v2.0 semantics: 0..10 = LIMIT 10 OFFSET 0
508                //                 20..30 = LIMIT 10 OFFSET 20
509                if let Some(e) = end {
510                    let limit = e - start;
511                    let offset = *start;
512                    // We need to return multiple cages, but our current structure
513                    // returns one. For now, prioritize LIMIT if offset is 0,
514                    // otherwise use OFFSET.
515                    if offset == 0 {
516                        return Ok((input, Cage {
517                            kind: CageKind::Limit(limit),
518                            conditions: vec![],
519                            logical_op: LogicalOp::And,
520                        }));
521                    } else {
522                        // Store limit in conditions as a workaround? No, just return offset.
523                        // Actually, let's return a compound cage. But the AST doesn't support that.
524                        // For proper v2.0, we'd need to extend the AST or return Vec<Cage>.
525                        // For now: return LIMIT with offset stored somehow.
526                        // Workaround: return the cage kind that combines both.
527                        return Ok((input, Cage {
528                            kind: CageKind::Limit(limit),
529                            conditions: vec![Condition {
530                                column: "__offset__".to_string(),
531                                op: Operator::Eq,
532                                value: Value::Int(offset as i64),
533                                is_array_unnest: false,
534                            }],
535                            logical_op: LogicalOp::And,
536                        }));
537                    }
538                } else {
539                    // Just offset
540                    return Ok((input, Cage {
541                        kind: CageKind::Offset(*start),
542                        conditions: vec![],
543                        logical_op: LogicalOp::And,
544                    }));
545                }
546            }
547            BlockItem::Sort(col, order) => {
548                return Ok((input, Cage {
549                    kind: CageKind::Sort(*order),
550                    conditions: vec![Condition {
551                        column: col.clone(),
552                        op: Operator::Eq,
553                        value: Value::Null,
554                        is_array_unnest: false,
555                    }],
556                    logical_op: LogicalOp::And,
557                }));
558            }
559            BlockItem::LegacyLimit(n) => {
560                return Ok((input, Cage {
561                    kind: CageKind::Limit(*n),
562                    conditions: vec![],
563                    logical_op: LogicalOp::And,
564                }));
565            }
566            BlockItem::LegacyOffset(n) => {
567                return Ok((input, Cage {
568                    kind: CageKind::Offset(*n),
569                    conditions: vec![],
570                    logical_op: LogicalOp::And,
571                }));
572            }
573            BlockItem::Filter(cond, op) => {
574                conditions.push(cond.clone());
575                logical_op = *op;
576            }
577        }
578    }
579    
580    // If we have conditions, return a filter cage
581    if !conditions.is_empty() {
582        Ok((input, Cage {
583            kind: CageKind::Filter,
584            conditions,
585            logical_op,
586        }))
587    } else {
588        // Empty block - return empty filter
589        Ok((input, Cage {
590            kind: CageKind::Filter,
591            conditions: vec![],
592            logical_op: LogicalOp::And,
593        }))
594    }
595}
596
597/// Parse operator and value together.
598fn parse_operator_and_value(input: &str) -> IResult<&str, (Operator, Value)> {
599    alt((
600        // Fuzzy match: ~value
601        map(preceded(char('~'), preceded(ws_or_comment, parse_value)), |v| (Operator::Fuzzy, v)),
602        // v2.0 Equal: ==value (try before >=)
603        map(preceded(tag("=="), preceded(ws_or_comment, parse_value)), |v| (Operator::Eq, v)),
604        // Greater than or equal: >=value
605        map(preceded(tag(">="), preceded(ws_or_comment, parse_value)), |v| (Operator::Gte, v)),
606        // Less than or equal: <=value
607        map(preceded(tag("<="), preceded(ws_or_comment, parse_value)), |v| (Operator::Lte, v)),
608        // Not equal: !=value
609        map(preceded(tag("!="), preceded(ws_or_comment, parse_value)), |v| (Operator::Ne, v)),
610        // Greater than: >value
611        map(preceded(char('>'), preceded(ws_or_comment, parse_value)), |v| (Operator::Gt, v)),
612        // Less than: <value
613        map(preceded(char('<'), preceded(ws_or_comment, parse_value)), |v| (Operator::Lt, v)),
614        // Legacy Equal: =value (backwards compat)
615        map(preceded(char('='), preceded(ws_or_comment, parse_value)), |v| (Operator::Eq, v)),
616    ))(input)
617}
618
619/// Parse a value.
620fn parse_value(input: &str) -> IResult<&str, Value> {
621    let (input, _) = ws_or_comment(input)?;
622    
623    alt((
624        // Parameter: $1, $2, etc.
625        map(preceded(char('$'), digit1), |n: &str| {
626            Value::Param(n.parse().unwrap_or(1))
627        }),
628        // Boolean: true/false
629        value(Value::Bool(true), tag("true")),
630        value(Value::Bool(false), tag("false")),
631        // Function call: name(args)
632        parse_function_call,
633        // Function without parens: now, etc. (keyword-like)
634        map(tag("now"), |_| Value::Function("now".to_string())),
635        // Number (float or int)
636        parse_number,
637        // Double-quoted string (v2.0 style)
638        parse_double_quoted_string,
639        // Single-quoted string (legacy)
640        parse_quoted_string,
641        // Bare identifier (treated as string)
642        map(parse_identifier, |s| Value::String(s.to_string())),
643    ))(input)
644}
645
646/// Parse function call: name(arg1, arg2)
647fn parse_function_call(input: &str) -> IResult<&str, Value> {
648    let (input, name) = parse_identifier(input)?;
649    let (input, _) = char('(')(input)?;
650    let (input, _) = ws_or_comment(input)?;
651    let (input, args) = opt(tuple((
652        parse_value,
653        many0(preceded(
654            tuple((ws_or_comment, char(','), ws_or_comment)),
655            parse_value
656        ))
657    )))(input)?;
658    let (input, _) = ws_or_comment(input)?;
659    let (input, _) = char(')')(input)?;
660
661    let params = match args {
662        Some((first, mut rest)) => {
663            let mut v = vec![first];
664            v.append(&mut rest);
665            v
666        },
667        None => vec![],
668    };
669
670    Ok((input, Value::Function(format!("{}({})", name, params.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", ")))))
671}
672
673/// Parse a number (integer or float).
674fn parse_number(input: &str) -> IResult<&str, Value> {
675    let (input, num_str) = recognize(tuple((
676        opt(char('-')),
677        digit1,
678        opt(pair(char('.'), digit1)),
679    )))(input)?;
680    
681    if num_str.contains('.') {
682        Ok((input, Value::Float(num_str.parse().unwrap_or(0.0))))
683    } else {
684        Ok((input, Value::Int(num_str.parse().unwrap_or(0))))
685    }
686}
687
688/// Parse a single-quoted string (legacy).
689fn parse_quoted_string(input: &str) -> IResult<&str, Value> {
690    let (input, _) = char('\'')(input)?;
691    let (input, content) = take_while(|c| c != '\'')(input)?;
692    let (input, _) = char('\'')(input)?;
693    
694    Ok((input, Value::String(content.to_string())))
695}
696
697/// Parse a double-quoted string (v2.0 style).
698fn parse_double_quoted_string(input: &str) -> IResult<&str, Value> {
699    let (input, _) = char('"')(input)?;
700    let (input, content) = take_while(|c| c != '"')(input)?;
701    let (input, _) = char('"')(input)?;
702    
703    Ok((input, Value::String(content.to_string())))
704}
705
706/// Parse legacy sort cage [^col] or [^!col] for window functions.
707fn parse_legacy_sort_cage(input: &str) -> IResult<&str, Cage> {
708    let (input, _) = char('^')(input)?;
709    let (input, desc) = opt(char('!'))(input)?;
710    let (input, col) = parse_identifier(input)?;
711    
712    let order = if desc.is_some() {
713        SortOrder::Desc
714    } else {
715        SortOrder::Asc
716    };
717    
718    Ok((
719        input,
720        Cage {
721            kind: CageKind::Sort(order),
722            conditions: vec![Condition {
723                column: col.to_string(),
724                op: Operator::Eq,
725                value: Value::Null,
726                is_array_unnest: false,
727            }],
728            logical_op: LogicalOp::And,
729        },
730    ))
731}
732
733fn parse_partition_block(input: &str) -> IResult<&str, Vec<String>> {
734    let (input, _) = char('{')(input)?;
735    let (input, _) = ws_or_comment(input)?;
736    let (input, _) = tag("Part")(input)?;
737    let (input, _) = ws_or_comment(input)?;
738    let (input, _) = char('=')(input)?;
739    let (input, _) = ws_or_comment(input)?;
740    
741    let (input, first) = parse_identifier(input)?;
742    let (input, rest) = many0(preceded(
743        tuple((ws_or_comment, char(','), ws_or_comment)),
744        parse_identifier
745    ))(input)?;
746    
747    let (input, _) = ws_or_comment(input)?;
748    let (input, _) = char('}')(input)?;
749    
750    let mut cols = vec![first.to_string()];
751    cols.append(&mut rest.iter().map(|s| s.to_string()).collect());
752    Ok((input, cols))
753}
754
755// ============================================================================
756// TESTS
757// ============================================================================
758
759#[cfg(test)]
760mod tests {
761    use super::*;
762
763    // ========================================================================
764    // v2.0 Syntax Tests
765    // ========================================================================
766
767    #[test]
768    fn test_v2_simple_get() {
769        let cmd = parse("get::users:'_").unwrap();
770        assert_eq!(cmd.action, Action::Get);
771        assert_eq!(cmd.table, "users");
772        assert_eq!(cmd.columns, vec![Column::Star]);
773    }
774
775    #[test]
776    fn test_v2_get_with_columns() {
777        let cmd = parse("get::users:'id'email").unwrap();
778        assert_eq!(cmd.action, Action::Get);
779        assert_eq!(cmd.table, "users");
780        assert_eq!(
781            cmd.columns,
782            vec![
783                Column::Named("id".to_string()),
784                Column::Named("email".to_string()),
785            ]
786        );
787    }
788
789    #[test]
790    fn test_v2_get_with_filter() {
791        let cmd = parse("get::users:'_ [ 'active == true ]").unwrap();
792        assert_eq!(cmd.cages.len(), 1);
793        assert_eq!(cmd.cages[0].kind, CageKind::Filter);
794        assert_eq!(cmd.cages[0].conditions.len(), 1);
795        assert_eq!(cmd.cages[0].conditions[0].column, "active");
796        assert_eq!(cmd.cages[0].conditions[0].op, Operator::Eq);
797        assert_eq!(cmd.cages[0].conditions[0].value, Value::Bool(true));
798    }
799
800    #[test]
801    fn test_v2_get_with_range_limit() {
802        let cmd = parse("get::users:'_ [ 0..10 ]").unwrap();
803        assert_eq!(cmd.cages.len(), 1);
804        assert_eq!(cmd.cages[0].kind, CageKind::Limit(10));
805    }
806
807    #[test]
808    fn test_v2_get_with_range_offset() {
809        let cmd = parse("get::users:'_ [ 20..30 ]").unwrap();
810        assert_eq!(cmd.cages.len(), 1);
811        // Range 20..30 = LIMIT 10 with offset 20
812        assert_eq!(cmd.cages[0].kind, CageKind::Limit(10));
813        // Offset stored in conditions as workaround
814        assert_eq!(cmd.cages[0].conditions[0].column, "__offset__");
815        assert_eq!(cmd.cages[0].conditions[0].value, Value::Int(20));
816    }
817
818    #[test]
819    fn test_v2_get_with_sort_desc() {
820        let cmd = parse("get::users:'_ [ -created_at ]").unwrap();
821        assert_eq!(cmd.cages.len(), 1);
822        assert_eq!(cmd.cages[0].kind, CageKind::Sort(SortOrder::Desc));
823        assert_eq!(cmd.cages[0].conditions[0].column, "created_at");
824    }
825
826    #[test]
827    fn test_v2_get_with_sort_asc() {
828        let cmd = parse("get::users:'_ [ +id ]").unwrap();
829        assert_eq!(cmd.cages.len(), 1);
830        assert_eq!(cmd.cages[0].kind, CageKind::Sort(SortOrder::Asc));
831        assert_eq!(cmd.cages[0].conditions[0].column, "id");
832    }
833
834    #[test]
835    fn test_v2_fuzzy_match() {
836        let cmd = parse("get::users:'id [ 'name ~ \"john\" ]").unwrap();
837        assert_eq!(cmd.cages[0].conditions[0].op, Operator::Fuzzy);
838        assert_eq!(cmd.cages[0].conditions[0].value, Value::String("john".to_string()));
839    }
840
841    #[test]
842    fn test_v2_param_in_filter() {
843        let cmd = parse("get::users:'id [ 'email == $1 ]").unwrap();
844        assert_eq!(cmd.cages.len(), 1);
845        assert_eq!(cmd.cages[0].conditions[0].value, Value::Param(1));
846    }
847
848    #[test]
849    fn test_v2_left_join() {
850        // Joins come directly after table name, not after columns
851        let cmd = parse("get::users<-posts:'id'title").unwrap();
852        assert_eq!(cmd.joins.len(), 1);
853        assert_eq!(cmd.joins[0].table, "posts");
854        assert_eq!(cmd.joins[0].kind, JoinKind::Left);
855    }
856
857    #[test]
858    fn test_v2_inner_join() {
859        let cmd = parse("get::users->posts:'id'title").unwrap();
860        assert_eq!(cmd.joins.len(), 1);
861        assert_eq!(cmd.joins[0].table, "posts");
862        assert_eq!(cmd.joins[0].kind, JoinKind::Inner);
863    }
864
865    #[test]
866    fn test_v2_right_join() {
867        let cmd = parse("get::orders->>customers:'_").unwrap();
868        assert_eq!(cmd.joins.len(), 1);
869        assert_eq!(cmd.joins[0].table, "customers");
870        assert_eq!(cmd.joins[0].kind, JoinKind::Right);
871    }
872
873    // ========================================================================
874    // Legacy Syntax Tests (backwards compatibility)
875    // ========================================================================
876
877    #[test]
878    fn test_legacy_simple_get() {
879        let cmd = parse("get::users:@*").unwrap();
880        assert_eq!(cmd.action, Action::Get);
881        assert_eq!(cmd.table, "users");
882        assert_eq!(cmd.columns, vec![Column::Star]);
883    }
884
885    #[test]
886    fn test_legacy_get_with_columns() {
887        let cmd = parse("get::users:@id@email@role").unwrap();
888        assert_eq!(cmd.action, Action::Get);
889        assert_eq!(cmd.table, "users");
890        assert_eq!(
891            cmd.columns,
892            vec![
893                Column::Named("id".to_string()),
894                Column::Named("email".to_string()),
895                Column::Named("role".to_string()),
896            ]
897        );
898    }
899
900    #[test]
901    fn test_legacy_get_with_filter() {
902        let cmd = parse("get::users:@*[active=true]").unwrap();
903        assert_eq!(cmd.cages.len(), 1);
904        assert_eq!(cmd.cages[0].kind, CageKind::Filter);
905        assert_eq!(cmd.cages[0].conditions.len(), 1);
906        assert_eq!(cmd.cages[0].conditions[0].column, "active");
907        assert_eq!(cmd.cages[0].conditions[0].op, Operator::Eq);
908        assert_eq!(cmd.cages[0].conditions[0].value, Value::Bool(true));
909    }
910
911    #[test]
912    fn test_legacy_get_with_limit() {
913        let cmd = parse("get::users:@*[lim=10]").unwrap();
914        assert_eq!(cmd.cages.len(), 1);
915        assert_eq!(cmd.cages[0].kind, CageKind::Limit(10));
916    }
917
918    #[test]
919    fn test_legacy_get_with_sort_desc() {
920        let cmd = parse("get::users:@*[^!created_at]").unwrap();
921        assert_eq!(cmd.cages.len(), 1);
922        assert_eq!(cmd.cages[0].kind, CageKind::Sort(SortOrder::Desc));
923    }
924
925    #[test]
926    fn test_set_command() {
927        let cmd = parse("set::users:[verified=true][id=$1]").unwrap();
928        assert_eq!(cmd.action, Action::Set);
929        assert_eq!(cmd.table, "users");
930        assert_eq!(cmd.cages.len(), 2);
931    }
932
933    #[test]
934    fn test_del_command() {
935        let cmd = parse("del::sessions:[expired_at<now]").unwrap();
936        assert_eq!(cmd.action, Action::Del);
937        assert_eq!(cmd.table, "sessions");
938    }
939
940    #[test]
941    fn test_legacy_fuzzy_match() {
942        let cmd = parse("get::users:@*[name~$1]").unwrap();
943        assert_eq!(cmd.cages[0].conditions[0].op, Operator::Fuzzy);
944    }
945
946    #[test]
947    fn test_legacy_complex_query() {
948        let cmd = parse("get::users:@id@email@role[active=true][lim=10]").unwrap();
949        assert_eq!(cmd.action, Action::Get);
950        assert_eq!(cmd.table, "users");
951        assert_eq!(cmd.columns.len(), 3);
952        assert_eq!(cmd.cages.len(), 2);
953    }
954
955    #[test]
956    fn test_legacy_param_in_filter() {
957        let cmd = parse("get::users:@*[id=$1]").unwrap();
958        assert_eq!(cmd.cages.len(), 1);
959        assert_eq!(cmd.cages[0].conditions[0].value, Value::Param(1));
960    }
961
962    #[test]
963    fn test_legacy_param_in_update() {
964        let cmd = parse("set::users:[verified=true][id=$1]").unwrap();
965        assert_eq!(cmd.action, Action::Set);
966        assert_eq!(cmd.cages.len(), 2);
967        assert_eq!(cmd.cages[1].conditions[0].value, Value::Param(1));
968    }
969}