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_until, take_while, take_while1},
39    character::complete::{char, digit1, multispace0, multispace1, not_line_ending},
40    combinator::{map, opt, recognize, value},
41    multi::{many0, separated_list1},
42    sequence::{delimited, 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    
84    // Special handling for INDEX action
85    if action == Action::Index {
86        return parse_index_command(input);
87    }
88    
89    let (input, table) = parse_identifier(input)?;
90    let (input, joins) = parse_joins(input)?;
91    let (input, _) = ws_or_comment(input)?;
92    // Link character ':' is optional - connects table to columns
93    let (input, _) = opt(char(':'))(input)?;
94    let (input, _) = ws_or_comment(input)?;
95    let (input, columns) = parse_columns(input)?;
96    let (input, _) = ws_or_comment(input)?;
97    
98    // Parse table-level constraints for Make action
99    let (input, table_constraints) = if action == Action::Make {
100        parse_table_constraints(input)?
101    } else {
102        (input, vec![])
103    };
104    
105    let (input, _) = ws_or_comment(input)?;
106    let (input, cages) = parse_unified_blocks(input)?;
107
108    Ok((
109        input,
110        QailCmd {
111            action,
112            table: table.to_string(),
113            joins,
114            columns,
115            cages,
116            distinct,
117            index_def: None,
118            table_constraints,
119        },
120    ))
121}
122
123/// Parse the action (get, set, del, add, gen, make, mod, over, with).
124fn parse_action(input: &str) -> IResult<&str, Action> {
125    alt((
126        value(Action::Get, tag("get")),
127        value(Action::Set, tag("set")),
128        value(Action::Del, tag("del")),
129        value(Action::Add, tag("add")),
130        value(Action::Gen, tag("gen")),
131        value(Action::Make, tag("make")),
132        value(Action::Mod, tag("mod")),
133        value(Action::Over, tag("over")),
134        value(Action::With, tag("with")),
135        value(Action::Index, tag("index")),
136    ))(input)
137}
138
139/// Parse an identifier (table name, column name).
140fn parse_identifier(input: &str) -> IResult<&str, &str> {
141    take_while1(|c: char| c.is_alphanumeric() || c == '_')(input)
142}
143
144fn parse_joins(input: &str) -> IResult<&str, Vec<Join>> {
145    many0(parse_single_join)(input)
146}
147
148/// Parse a single join: `->` (INNER), `<-` (LEFT), `->>` (RIGHT)
149fn parse_single_join(input: &str) -> IResult<&str, Join> {
150    let (input, _) = ws_or_comment(input)?;
151    
152    // Try RIGHT JOIN first (->>)
153    if let Ok((remaining, _)) = tag::<_, _, nom::error::Error<&str>>("->>") (input) {
154        let (remaining, _) = ws_or_comment(remaining)?;
155        let (remaining, table) = parse_identifier(remaining)?;
156        return Ok((remaining, Join {
157            table: table.to_string(),
158            kind: JoinKind::Right,
159        }));
160    }
161    
162    // Try LEFT JOIN (<-)
163    if let Ok((remaining, _)) = tag::<_, _, nom::error::Error<&str>>("<-") (input) {
164        let (remaining, _) = ws_or_comment(remaining)?;
165        let (remaining, table) = parse_identifier(remaining)?;
166        return Ok((remaining, Join {
167            table: table.to_string(),
168            kind: JoinKind::Left,
169        }));
170    }
171    
172    // Default: INNER JOIN (->)
173    let (input, _) = tag("->")(input)?;
174    let (input, _) = ws_or_comment(input)?;
175    let (input, table) = parse_identifier(input)?;
176    Ok((input, Join {
177        table: table.to_string(),
178        kind: JoinKind::Inner,
179    }))
180}
181
182/// Parse columns using the v2.0 label syntax ('col).
183fn parse_columns(input: &str) -> IResult<&str, Vec<Column>> {
184    many0(preceded(ws_or_comment, parse_any_column))(input)
185}
186
187fn parse_any_column(input: &str) -> IResult<&str, Column> {
188    alt((
189        // v2.0 Label: 'col...
190        preceded(char('\''), parse_label_column),
191        // Legacy Hook: @col... (for backwards compat during migration)
192        preceded(char('@'), parse_at_column),
193    ))(input)
194}
195
196/// Parse a column with the v2.0 label syntax ('col).
197fn parse_label_column(input: &str) -> IResult<&str, Column> {
198    alt((
199        // Wildcard: '_ for all columns
200        value(Column::Star, char('_')),
201        // Named or complex column
202        parse_column_full_def_or_named,
203    ))(input)
204}
205
206/// Legacy @ column parsing for backwards compatibility.
207fn parse_at_column(input: &str) -> IResult<&str, Column> {
208    alt((
209        value(Column::Star, char('*')),
210        // Check for drop via @-name convention
211        map(preceded(char('-'), parse_identifier), |name| Column::Mod { 
212            kind: ModKind::Drop, 
213            col: Box::new(Column::Named(name.to_string())) 
214        }),
215        parse_column_full_def_or_named, 
216    ))(input)
217}
218
219fn parse_column_full_def_or_named(input: &str) -> IResult<&str, Column> {
220    // 1. Parse Name
221    let (input, name) = parse_identifier(input)?;
222    
223    // 2. Opt: Aggregates (#func)
224    if let Ok((input, Some(func))) = opt(preceded(char('#'), parse_agg_func))(input) {
225        return Ok((input, Column::Aggregate {
226             col: name.to_string(),
227             func
228        }));
229    }
230    
231    // 3. Opt: check for colon (type definition)
232    if let Ok((input, _)) = char::<_, nom::error::Error<&str>>(':')(input) {
233        // We have a type OR a window function.
234        let (input, type_or_func) = parse_identifier(input)?;
235        
236        let (input, _) = ws_or_comment(input)?;
237        
238        // Peek/Check for open paren `(` for window function
239        if let Ok((input, _)) = char::<_, nom::error::Error<&str>>('(')(input) {
240            // It IS a function call -> Window Column
241            let (input, _) = ws_or_comment(input)?;
242            let (input, args) = opt(tuple((
243                parse_value,
244                many0(preceded(
245                    tuple((ws_or_comment, char(','), ws_or_comment)),
246                    parse_value
247                ))
248            )))(input)?;
249            let (input, _) = ws_or_comment(input)?;
250            let (input, _) = char(')')(input)?;
251            
252            let params = match args {
253                Some((first, mut rest)) => {
254                    let mut v = vec![first];
255                    v.append(&mut rest);
256                    v
257                },
258                None => vec![],
259            };
260
261            // Parse Order Cages (e.g. ^!amount) - legacy syntax
262            let (input, sorts) = many0(parse_legacy_sort_cage)(input)?;
263            
264            // Parse Partition: {Part=...}
265            let (input, partitions) = opt(parse_partition_block)(input)?;
266            let partition = partitions.unwrap_or_default();
267
268            return Ok((input, Column::Window {
269                name: name.to_string(),
270                func: type_or_func.to_string(),
271                params,
272                partition,
273                order: sorts,
274            }));
275        } else {
276            // It is just a Type Definition
277            let (input, constraints) = parse_constraints(input)?;
278            
279            return Ok((input, Column::Def { 
280                name: name.to_string(), 
281                data_type: type_or_func.to_string(), 
282                constraints 
283            }));
284        }
285    }
286    
287    // No colon, check for constraints (inferred type Def)
288    let (input, constraints) = parse_constraints(input)?;
289    if !constraints.is_empty() {
290         Ok((input, Column::Def { 
291            name: name.to_string(), 
292            data_type: "str".to_string(), 
293            constraints 
294        }))
295    } else {
296        // Just a named column
297        Ok((input, Column::Named(name.to_string())))
298    }
299}
300
301fn parse_constraints(input: &str) -> IResult<&str, Vec<Constraint>> {
302    many0(alt((
303        // ^pk without parentheses (column-level PK)
304        map(
305            tuple((tag("^pk"), nom::combinator::not(char('(')))),
306            |_| Constraint::PrimaryKey
307        ),
308        // ^uniq without following 'ue(' (to avoid matching ^unique())
309        map(
310            tuple((tag("^uniq"), nom::combinator::not(tag("ue(")))),
311            |_| Constraint::Unique
312        ),
313        value(Constraint::Nullable, char('?')),
314        parse_default_constraint,
315        parse_check_constraint,
316        parse_comment_constraint,
317    )))(input)
318}
319
320/// Parse DEFAULT value constraint: `= value` or `= func()`
321fn parse_default_constraint(input: &str) -> IResult<&str, Constraint> {
322    let (input, _) = preceded(multispace0, char('='))(input)?;
323    let (input, _) = multispace0(input)?;
324    
325    // Parse function call like uuid(), now(), or literal values
326    let (input, value) = alt((
327        // Function call: name()
328        map(
329            pair(
330                take_while1(|c: char| c.is_alphanumeric() || c == '_'),
331                tag("()")
332            ),
333            |(name, parens): (&str, &str)| format!("{}{}", name, parens)
334        ),
335        // Numeric literal
336        map(
337            take_while1(|c: char| c.is_numeric() || c == '.' || c == '-'),
338            |s: &str| s.to_string()
339        ),
340        // Quoted string
341        map(
342            delimited(char('"'), take_until("\""), char('"')),
343            |s: &str| format!("'{}'", s)
344        ),
345    ))(input)?;
346    
347    Ok((input, Constraint::Default(value)))
348}
349
350/// Parse CHECK constraint: `^check("a","b","c")`
351fn parse_check_constraint(input: &str) -> IResult<&str, Constraint> {
352    let (input, _) = tag("^check(")(input)?;
353    let (input, values) = separated_list1(
354        char(','),
355        delimited(
356            multispace0,
357            delimited(char('"'), take_until("\""), char('"')),
358            multispace0
359        )
360    )(input)?;
361    let (input, _) = char(')')(input)?;
362    
363    Ok((input, Constraint::Check(values.into_iter().map(|s| s.to_string()).collect())))
364}
365
366/// Parse COMMENT constraint: `^comment("description")`
367fn parse_comment_constraint(input: &str) -> IResult<&str, Constraint> {
368    let (input, _) = tag("^comment(\"")(input)?;
369    let (input, text) = take_until("\"")(input)?;
370    let (input, _) = tag("\")")(input)?;
371    Ok((input, Constraint::Comment(text.to_string())))
372}
373
374/// Parse INDEX command: `index::idx_name^on(table:'col1-col2)^unique`
375/// Returns a QailCmd with action=Index and populated index_def
376fn parse_index_command(input: &str) -> IResult<&str, QailCmd> {
377    // Parse index name
378    let (input, name) = parse_identifier(input)?;
379    
380    // Parse ^on(table:'columns)
381    let (input, _) = tag("^on(")(input)?;
382    let (input, table) = parse_identifier(input)?;
383    let (input, _) = char(':')(input)?;
384    let (input, columns) = parse_index_columns(input)?;
385    let (input, _) = char(')')(input)?;
386    
387    // Parse optional ^unique
388    let (input, unique) = opt(tag("^unique"))(input)?;
389    
390    Ok((input, QailCmd {
391        action: Action::Index,
392        table: table.to_string(),
393        columns: vec![],
394        joins: vec![],
395        cages: vec![],
396        distinct: false,
397        index_def: Some(IndexDef {
398            name: name.to_string(),
399            table: table.to_string(),
400            columns,
401            unique: unique.is_some(),
402        }),
403        table_constraints: vec![],
404    }))
405}
406
407/// Parse index columns: 'col1-col2-col3
408fn parse_index_columns(input: &str) -> IResult<&str, Vec<String>> {
409    let (input, _) = char('\'')(input)?;
410    let (input, first) = parse_identifier(input)?;
411    let (input, rest) = many0(preceded(char('-'), parse_identifier))(input)?;
412    
413    let mut cols = vec![first.to_string()];
414    cols.extend(rest.iter().map(|s| s.to_string()));
415    Ok((input, cols))
416}
417
418/// Parse table-level constraints: ^unique(col1, col2) or ^pk(col1, col2)
419fn parse_table_constraints(input: &str) -> IResult<&str, Vec<TableConstraint>> {
420    many0(alt((
421        parse_table_unique,
422        parse_table_pk,
423    )))(input)
424}
425
426/// Parse ^unique(col1, col2)
427fn parse_table_unique(input: &str) -> IResult<&str, TableConstraint> {
428    let (input, _) = tag("^unique(")(input)?;
429    let (input, cols) = parse_constraint_columns(input)?;
430    let (input, _) = char(')')(input)?;
431    Ok((input, TableConstraint::Unique(cols)))
432}
433
434/// Parse ^pk(col1, col2)
435fn parse_table_pk(input: &str) -> IResult<&str, TableConstraint> {
436    let (input, _) = tag("^pk(")(input)?;
437    let (input, cols) = parse_constraint_columns(input)?;
438    let (input, _) = char(')')(input)?;
439    Ok((input, TableConstraint::PrimaryKey(cols)))
440}
441
442/// Parse comma-separated column names: col1, col2, col3
443fn parse_constraint_columns(input: &str) -> IResult<&str, Vec<String>> {
444    let (input, _) = multispace0(input)?;
445    let (input, first) = parse_identifier(input)?;
446    let (input, rest) = many0(preceded(
447        tuple((multispace0, char(','), multispace0)),
448        parse_identifier
449    ))(input)?;
450    let (input, _) = multispace0(input)?;
451    
452    let mut cols = vec![first.to_string()];
453    cols.extend(rest.iter().map(|s| s.to_string()));
454    Ok((input, cols))
455}
456
457fn parse_agg_func(input: &str) -> IResult<&str, AggregateFunc> {
458    alt((
459        value(AggregateFunc::Count, tag("count")),
460        value(AggregateFunc::Sum, tag("sum")),
461        value(AggregateFunc::Avg, tag("avg")),
462        value(AggregateFunc::Min, tag("min")),
463        value(AggregateFunc::Max, tag("max")),
464    ))(input)
465}
466
467/// Parse unified constraint blocks [...].
468/// v2.0 syntax: [ 'active == true, -created_at, 0..10 ]
469fn parse_unified_blocks(input: &str) -> IResult<&str, Vec<Cage>> {
470    many0(preceded(ws_or_comment, parse_unified_block))(input)
471}
472
473/// Parse a unified constraint block [...].
474/// Contains comma-separated items: filters, sorts, ranges.
475fn parse_unified_block(input: &str) -> IResult<&str, Cage> {
476    let (input, _) = char('[')(input)?;
477    let (input, _) = ws_or_comment(input)?;
478    
479    // Parse all items in the block (comma-separated)
480    let (input, items) = parse_block_items(input)?;
481    
482    let (input, _) = ws_or_comment(input)?;
483    let (input, _) = char(']')(input)?;
484    
485    // Convert items into appropriate cages
486    // For now, combine into a single cage or return the first meaningful one
487    items_to_cage(items, input)
488}
489
490/// Represents a parsed item within a unified block.
491#[derive(Debug)]
492enum BlockItem {
493    Filter(Condition, LogicalOp),
494    Sort(String, SortOrder),
495    Range(usize, Option<usize>), // start, end
496    LegacyLimit(usize),
497    LegacyOffset(usize),
498}
499
500/// Parse comma-separated items within a block.
501/// Also handles | (OR) and & (AND) operators for filter conditions.
502fn parse_block_items(input: &str) -> IResult<&str, Vec<BlockItem>> {
503    let (input, first) = opt(parse_block_item)(input)?;
504    
505    match first {
506        None => Ok((input, vec![])),
507        Some(mut item) => {
508            let mut items = vec![];
509            let mut remaining = input;
510            
511            loop {
512                let (input, _) = ws_or_comment(remaining)?;
513                
514                // Check for various separators: comma, pipe (OR), ampersand (AND)
515                if let Ok((input, _)) = char::<_, nom::error::Error<&str>>(',')(input) {
516                    // Comma separator - add current item and parse next
517                    items.push(item);
518                    let (input, _) = ws_or_comment(input)?;
519                    let (input, next_item) = parse_block_item(input)?;
520                    item = next_item;
521                    remaining = input;
522                } else if let Ok((new_input, _)) = char::<_, nom::error::Error<&str>>('|')(input) {
523                    // OR separator - update item's logical op and parse next filter
524                    if let BlockItem::Filter(cond, _) = item {
525                        items.push(BlockItem::Filter(cond, LogicalOp::Or));
526                    } else {
527                        items.push(item);
528                    }
529                    let (new_input, _) = ws_or_comment(new_input)?;
530                    let (new_input, next_item) = parse_filter_item(new_input)?;
531                    // Mark the next item as part of an OR chain
532                    if let BlockItem::Filter(cond, _) = next_item {
533                        item = BlockItem::Filter(cond, LogicalOp::Or);
534                    } else {
535                        item = next_item;
536                    }
537                    remaining = new_input;
538                } else if let Ok((new_input, _)) = char::<_, nom::error::Error<&str>>('&')(input) {
539                    // AND separator
540                    items.push(item);
541                    let (new_input, _) = ws_or_comment(new_input)?;
542                    let (new_input, next_item) = parse_filter_item(new_input)?;
543                    item = next_item;
544                    remaining = new_input;
545                } else {
546                    items.push(item);
547                    remaining = input;
548                    break;
549                }
550            }
551            
552            Ok((remaining, items))
553        }
554    }
555}
556
557/// Parse a single item in a unified block.
558fn parse_block_item(input: &str) -> IResult<&str, BlockItem> {
559    alt((
560        // Range: N..M or N.. (must try before other number parsing)
561        parse_range_item,
562        // Sort: +col (asc) or -col (desc)
563        parse_sort_item,
564        // Legacy limit: lim=N
565        parse_legacy_limit_item,
566        // Legacy offset: off=N
567        parse_legacy_offset_item,
568        // Legacy sort: ^col or ^!col
569        parse_legacy_sort_item,
570        // Filter: 'col == value
571        parse_filter_item,
572    ))(input)
573}
574
575/// Parse a range item: N..M or N..
576fn parse_range_item(input: &str) -> IResult<&str, BlockItem> {
577    let (input, start) = digit1(input)?;
578    let (input, _) = tag("..")(input)?;
579    let (input, end) = opt(digit1)(input)?;
580    
581    let start_num: usize = start.parse().unwrap_or(0);
582    let end_num = end.map(|e| e.parse().unwrap_or(0));
583    
584    Ok((input, BlockItem::Range(start_num, end_num)))
585}
586
587/// Parse a sort item: +col (asc) or -col (desc).
588fn parse_sort_item(input: &str) -> IResult<&str, BlockItem> {
589    alt((
590        map(preceded(char('+'), parse_identifier), |col| {
591            BlockItem::Sort(col.to_string(), SortOrder::Asc)
592        }),
593        map(preceded(char('-'), parse_identifier), |col| {
594            BlockItem::Sort(col.to_string(), SortOrder::Desc)
595        }),
596    ))(input)
597}
598
599/// Parse legacy limit: lim=N
600fn parse_legacy_limit_item(input: &str) -> IResult<&str, BlockItem> {
601    let (input, _) = tag("lim")(input)?;
602    let (input, _) = ws_or_comment(input)?;
603    let (input, _) = char('=')(input)?;
604    let (input, _) = ws_or_comment(input)?;
605    let (input, n) = digit1(input)?;
606    Ok((input, BlockItem::LegacyLimit(n.parse().unwrap_or(10))))
607}
608
609/// Parse legacy offset: off=N
610fn parse_legacy_offset_item(input: &str) -> IResult<&str, BlockItem> {
611    let (input, _) = tag("off")(input)?;
612    let (input, _) = ws_or_comment(input)?;
613    let (input, _) = char('=')(input)?;
614    let (input, _) = ws_or_comment(input)?;
615    let (input, n) = digit1(input)?;
616    Ok((input, BlockItem::LegacyOffset(n.parse().unwrap_or(0))))
617}
618
619/// Parse legacy sort: ^col or ^!col
620fn parse_legacy_sort_item(input: &str) -> IResult<&str, BlockItem> {
621    let (input, _) = char('^')(input)?;
622    let (input, desc) = opt(char('!'))(input)?;
623    let (input, col) = parse_identifier(input)?;
624    
625    let order = if desc.is_some() {
626        SortOrder::Desc
627    } else {
628        SortOrder::Asc
629    };
630    
631    Ok((input, BlockItem::Sort(col.to_string(), order)))
632}
633
634/// Parse a filter item: 'col == value or col == value
635fn parse_filter_item(input: &str) -> IResult<&str, BlockItem> {
636    // Optional leading ' for column (v2.0 style)
637    let (input, _) = opt(char('\''))(input)?;
638    let (input, column) = parse_identifier(input)?;
639    
640    // Check for array unnest syntax: column[*]
641    let (input, is_array_unnest) = if input.starts_with("[*]") {
642        (&input[3..], true)
643    } else {
644        (input, false)
645    };
646    
647    let (input, _) = ws_or_comment(input)?;
648    let (input, (op, val)) = parse_operator_and_value(input)?;
649    
650    Ok((input, BlockItem::Filter(
651        Condition {
652            column: column.to_string(),
653            op,
654            value: val,
655            is_array_unnest,
656        },
657        LogicalOp::And, // Default, could be enhanced for | and &
658    )))
659}
660
661/// Convert parsed block items into a Cage.
662fn items_to_cage(items: Vec<BlockItem>, input: &str) -> IResult<&str, Cage> {
663    // Default: return a filter cage if we have filters
664    let mut conditions = Vec::new();
665    let mut logical_op = LogicalOp::And;
666    
667    // Check for special single-item cases
668    for item in &items {
669        match item {
670            BlockItem::Range(start, end) => {
671                // Range: start..end means OFFSET start, LIMIT (end - start)
672                // If end is None, it's just OFFSET start
673                // v2.0 semantics: 0..10 = LIMIT 10 OFFSET 0
674                //                 20..30 = LIMIT 10 OFFSET 20
675                if let Some(e) = end {
676                    let limit = e - start;
677                    let offset = *start;
678                    // We need to return multiple cages, but our current structure
679                    // returns one. For now, prioritize LIMIT if offset is 0,
680                    // otherwise use OFFSET.
681                    if offset == 0 {
682                        return Ok((input, Cage {
683                            kind: CageKind::Limit(limit),
684                            conditions: vec![],
685                            logical_op: LogicalOp::And,
686                        }));
687                    } else {
688                        // Store limit in conditions as a workaround? No, just return offset.
689                        // Actually, let's return a compound cage. But the AST doesn't support that.
690                        // For proper v2.0, we'd need to extend the AST or return Vec<Cage>.
691                        // For now: return LIMIT with offset stored somehow.
692                        // Workaround: return the cage kind that combines both.
693                        return Ok((input, Cage {
694                            kind: CageKind::Limit(limit),
695                            conditions: vec![Condition {
696                                column: "__offset__".to_string(),
697                                op: Operator::Eq,
698                                value: Value::Int(offset as i64),
699                                is_array_unnest: false,
700                            }],
701                            logical_op: LogicalOp::And,
702                        }));
703                    }
704                } else {
705                    // Just offset
706                    return Ok((input, Cage {
707                        kind: CageKind::Offset(*start),
708                        conditions: vec![],
709                        logical_op: LogicalOp::And,
710                    }));
711                }
712            }
713            BlockItem::Sort(col, order) => {
714                return Ok((input, Cage {
715                    kind: CageKind::Sort(*order),
716                    conditions: vec![Condition {
717                        column: col.clone(),
718                        op: Operator::Eq,
719                        value: Value::Null,
720                        is_array_unnest: false,
721                    }],
722                    logical_op: LogicalOp::And,
723                }));
724            }
725            BlockItem::LegacyLimit(n) => {
726                return Ok((input, Cage {
727                    kind: CageKind::Limit(*n),
728                    conditions: vec![],
729                    logical_op: LogicalOp::And,
730                }));
731            }
732            BlockItem::LegacyOffset(n) => {
733                return Ok((input, Cage {
734                    kind: CageKind::Offset(*n),
735                    conditions: vec![],
736                    logical_op: LogicalOp::And,
737                }));
738            }
739            BlockItem::Filter(cond, op) => {
740                conditions.push(cond.clone());
741                logical_op = *op;
742            }
743        }
744    }
745    
746    // If we have conditions, return a filter cage
747    if !conditions.is_empty() {
748        Ok((input, Cage {
749            kind: CageKind::Filter,
750            conditions,
751            logical_op,
752        }))
753    } else {
754        // Empty block - return empty filter
755        Ok((input, Cage {
756            kind: CageKind::Filter,
757            conditions: vec![],
758            logical_op: LogicalOp::And,
759        }))
760    }
761}
762
763/// Parse operator and value together.
764fn parse_operator_and_value(input: &str) -> IResult<&str, (Operator, Value)> {
765    alt((
766        // Fuzzy match: ~value
767        map(preceded(char('~'), preceded(ws_or_comment, parse_value)), |v| (Operator::Fuzzy, v)),
768        // v2.0 Equal: ==value (try before >=)
769        map(preceded(tag("=="), preceded(ws_or_comment, parse_value)), |v| (Operator::Eq, v)),
770        // Greater than or equal: >=value
771        map(preceded(tag(">="), preceded(ws_or_comment, parse_value)), |v| (Operator::Gte, v)),
772        // Less than or equal: <=value
773        map(preceded(tag("<="), preceded(ws_or_comment, parse_value)), |v| (Operator::Lte, v)),
774        // Not equal: !=value
775        map(preceded(tag("!="), preceded(ws_or_comment, parse_value)), |v| (Operator::Ne, v)),
776        // Greater than: >value
777        map(preceded(char('>'), preceded(ws_or_comment, parse_value)), |v| (Operator::Gt, v)),
778        // Less than: <value
779        map(preceded(char('<'), preceded(ws_or_comment, parse_value)), |v| (Operator::Lt, v)),
780        // Legacy Equal: =value (backwards compat)
781        map(preceded(char('='), preceded(ws_or_comment, parse_value)), |v| (Operator::Eq, v)),
782    ))(input)
783}
784
785/// Parse a value.
786fn parse_value(input: &str) -> IResult<&str, Value> {
787    let (input, _) = ws_or_comment(input)?;
788    
789    alt((
790        // Parameter: $1, $2, etc.
791        map(preceded(char('$'), digit1), |n: &str| {
792            Value::Param(n.parse().unwrap_or(1))
793        }),
794        // Boolean: true/false
795        value(Value::Bool(true), tag("true")),
796        value(Value::Bool(false), tag("false")),
797        // Function call: name(args)
798        parse_function_call,
799        // Function without parens: now, etc. (keyword-like)
800        map(tag("now"), |_| Value::Function("now".to_string())),
801        // Number (float or int)
802        parse_number,
803        // Double-quoted string (v2.0 style)
804        parse_double_quoted_string,
805        // Single-quoted string (legacy)
806        parse_quoted_string,
807        // Bare identifier (treated as string)
808        map(parse_identifier, |s| Value::String(s.to_string())),
809    ))(input)
810}
811
812/// Parse function call: name(arg1, arg2)
813fn parse_function_call(input: &str) -> IResult<&str, Value> {
814    let (input, name) = parse_identifier(input)?;
815    let (input, _) = char('(')(input)?;
816    let (input, _) = ws_or_comment(input)?;
817    let (input, args) = opt(tuple((
818        parse_value,
819        many0(preceded(
820            tuple((ws_or_comment, char(','), ws_or_comment)),
821            parse_value
822        ))
823    )))(input)?;
824    let (input, _) = ws_or_comment(input)?;
825    let (input, _) = char(')')(input)?;
826
827    let params = match args {
828        Some((first, mut rest)) => {
829            let mut v = vec![first];
830            v.append(&mut rest);
831            v
832        },
833        None => vec![],
834    };
835
836    Ok((input, Value::Function(format!("{}({})", name, params.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", ")))))
837}
838
839/// Parse a number (integer or float).
840fn parse_number(input: &str) -> IResult<&str, Value> {
841    let (input, num_str) = recognize(tuple((
842        opt(char('-')),
843        digit1,
844        opt(pair(char('.'), digit1)),
845    )))(input)?;
846    
847    if num_str.contains('.') {
848        Ok((input, Value::Float(num_str.parse().unwrap_or(0.0))))
849    } else {
850        Ok((input, Value::Int(num_str.parse().unwrap_or(0))))
851    }
852}
853
854/// Parse a single-quoted string (legacy).
855fn parse_quoted_string(input: &str) -> IResult<&str, Value> {
856    let (input, _) = char('\'')(input)?;
857    let (input, content) = take_while(|c| c != '\'')(input)?;
858    let (input, _) = char('\'')(input)?;
859    
860    Ok((input, Value::String(content.to_string())))
861}
862
863/// Parse a double-quoted string (v2.0 style).
864fn parse_double_quoted_string(input: &str) -> IResult<&str, Value> {
865    let (input, _) = char('"')(input)?;
866    let (input, content) = take_while(|c| c != '"')(input)?;
867    let (input, _) = char('"')(input)?;
868    
869    Ok((input, Value::String(content.to_string())))
870}
871
872/// Parse legacy sort cage [^col] or [^!col] for window functions.
873fn parse_legacy_sort_cage(input: &str) -> IResult<&str, Cage> {
874    let (input, _) = char('^')(input)?;
875    let (input, desc) = opt(char('!'))(input)?;
876    let (input, col) = parse_identifier(input)?;
877    
878    let order = if desc.is_some() {
879        SortOrder::Desc
880    } else {
881        SortOrder::Asc
882    };
883    
884    Ok((
885        input,
886        Cage {
887            kind: CageKind::Sort(order),
888            conditions: vec![Condition {
889                column: col.to_string(),
890                op: Operator::Eq,
891                value: Value::Null,
892                is_array_unnest: false,
893            }],
894            logical_op: LogicalOp::And,
895        },
896    ))
897}
898
899fn parse_partition_block(input: &str) -> IResult<&str, Vec<String>> {
900    let (input, _) = char('{')(input)?;
901    let (input, _) = ws_or_comment(input)?;
902    let (input, _) = tag("Part")(input)?;
903    let (input, _) = ws_or_comment(input)?;
904    let (input, _) = char('=')(input)?;
905    let (input, _) = ws_or_comment(input)?;
906    
907    let (input, first) = parse_identifier(input)?;
908    let (input, rest) = many0(preceded(
909        tuple((ws_or_comment, char(','), ws_or_comment)),
910        parse_identifier
911    ))(input)?;
912    
913    let (input, _) = ws_or_comment(input)?;
914    let (input, _) = char('}')(input)?;
915    
916    let mut cols = vec![first.to_string()];
917    cols.append(&mut rest.iter().map(|s| s.to_string()).collect());
918    Ok((input, cols))
919}
920
921// ============================================================================
922// TESTS
923// ============================================================================
924
925#[cfg(test)]
926mod tests {
927    use super::*;
928
929    // ========================================================================
930    // v2.0 Syntax Tests
931    // ========================================================================
932
933    #[test]
934    fn test_v2_simple_get() {
935        let cmd = parse("get::users:'_").unwrap();
936        assert_eq!(cmd.action, Action::Get);
937        assert_eq!(cmd.table, "users");
938        assert_eq!(cmd.columns, vec![Column::Star]);
939    }
940
941    #[test]
942    fn test_v2_get_with_columns() {
943        let cmd = parse("get::users:'id'email").unwrap();
944        assert_eq!(cmd.action, Action::Get);
945        assert_eq!(cmd.table, "users");
946        assert_eq!(
947            cmd.columns,
948            vec![
949                Column::Named("id".to_string()),
950                Column::Named("email".to_string()),
951            ]
952        );
953    }
954
955    #[test]
956    fn test_v2_get_with_filter() {
957        let cmd = parse("get::users:'_ [ 'active == true ]").unwrap();
958        assert_eq!(cmd.cages.len(), 1);
959        assert_eq!(cmd.cages[0].kind, CageKind::Filter);
960        assert_eq!(cmd.cages[0].conditions.len(), 1);
961        assert_eq!(cmd.cages[0].conditions[0].column, "active");
962        assert_eq!(cmd.cages[0].conditions[0].op, Operator::Eq);
963        assert_eq!(cmd.cages[0].conditions[0].value, Value::Bool(true));
964    }
965
966    #[test]
967    fn test_v2_get_with_range_limit() {
968        let cmd = parse("get::users:'_ [ 0..10 ]").unwrap();
969        assert_eq!(cmd.cages.len(), 1);
970        assert_eq!(cmd.cages[0].kind, CageKind::Limit(10));
971    }
972
973    #[test]
974    fn test_v2_get_with_range_offset() {
975        let cmd = parse("get::users:'_ [ 20..30 ]").unwrap();
976        assert_eq!(cmd.cages.len(), 1);
977        // Range 20..30 = LIMIT 10 with offset 20
978        assert_eq!(cmd.cages[0].kind, CageKind::Limit(10));
979        // Offset stored in conditions as workaround
980        assert_eq!(cmd.cages[0].conditions[0].column, "__offset__");
981        assert_eq!(cmd.cages[0].conditions[0].value, Value::Int(20));
982    }
983
984    #[test]
985    fn test_v2_get_with_sort_desc() {
986        let cmd = parse("get::users:'_ [ -created_at ]").unwrap();
987        assert_eq!(cmd.cages.len(), 1);
988        assert_eq!(cmd.cages[0].kind, CageKind::Sort(SortOrder::Desc));
989        assert_eq!(cmd.cages[0].conditions[0].column, "created_at");
990    }
991
992    #[test]
993    fn test_v2_get_with_sort_asc() {
994        let cmd = parse("get::users:'_ [ +id ]").unwrap();
995        assert_eq!(cmd.cages.len(), 1);
996        assert_eq!(cmd.cages[0].kind, CageKind::Sort(SortOrder::Asc));
997        assert_eq!(cmd.cages[0].conditions[0].column, "id");
998    }
999
1000    #[test]
1001    fn test_v2_fuzzy_match() {
1002        let cmd = parse("get::users:'id [ 'name ~ \"john\" ]").unwrap();
1003        assert_eq!(cmd.cages[0].conditions[0].op, Operator::Fuzzy);
1004        assert_eq!(cmd.cages[0].conditions[0].value, Value::String("john".to_string()));
1005    }
1006
1007    #[test]
1008    fn test_v2_param_in_filter() {
1009        let cmd = parse("get::users:'id [ 'email == $1 ]").unwrap();
1010        assert_eq!(cmd.cages.len(), 1);
1011        assert_eq!(cmd.cages[0].conditions[0].value, Value::Param(1));
1012    }
1013
1014    #[test]
1015    fn test_v2_left_join() {
1016        // Joins come directly after table name, not after columns
1017        let cmd = parse("get::users<-posts:'id'title").unwrap();
1018        assert_eq!(cmd.joins.len(), 1);
1019        assert_eq!(cmd.joins[0].table, "posts");
1020        assert_eq!(cmd.joins[0].kind, JoinKind::Left);
1021    }
1022
1023    #[test]
1024    fn test_v2_inner_join() {
1025        let cmd = parse("get::users->posts:'id'title").unwrap();
1026        assert_eq!(cmd.joins.len(), 1);
1027        assert_eq!(cmd.joins[0].table, "posts");
1028        assert_eq!(cmd.joins[0].kind, JoinKind::Inner);
1029    }
1030
1031    #[test]
1032    fn test_v2_right_join() {
1033        let cmd = parse("get::orders->>customers:'_").unwrap();
1034        assert_eq!(cmd.joins.len(), 1);
1035        assert_eq!(cmd.joins[0].table, "customers");
1036        assert_eq!(cmd.joins[0].kind, JoinKind::Right);
1037    }
1038
1039    // ========================================================================
1040    // Legacy Syntax Tests (backwards compatibility)
1041    // ========================================================================
1042
1043    #[test]
1044    fn test_legacy_simple_get() {
1045        let cmd = parse("get::users:@*").unwrap();
1046        assert_eq!(cmd.action, Action::Get);
1047        assert_eq!(cmd.table, "users");
1048        assert_eq!(cmd.columns, vec![Column::Star]);
1049    }
1050
1051    #[test]
1052    fn test_legacy_get_with_columns() {
1053        let cmd = parse("get::users:@id@email@role").unwrap();
1054        assert_eq!(cmd.action, Action::Get);
1055        assert_eq!(cmd.table, "users");
1056        assert_eq!(
1057            cmd.columns,
1058            vec![
1059                Column::Named("id".to_string()),
1060                Column::Named("email".to_string()),
1061                Column::Named("role".to_string()),
1062            ]
1063        );
1064    }
1065
1066    #[test]
1067    fn test_legacy_get_with_filter() {
1068        let cmd = parse("get::users:@*[active=true]").unwrap();
1069        assert_eq!(cmd.cages.len(), 1);
1070        assert_eq!(cmd.cages[0].kind, CageKind::Filter);
1071        assert_eq!(cmd.cages[0].conditions.len(), 1);
1072        assert_eq!(cmd.cages[0].conditions[0].column, "active");
1073        assert_eq!(cmd.cages[0].conditions[0].op, Operator::Eq);
1074        assert_eq!(cmd.cages[0].conditions[0].value, Value::Bool(true));
1075    }
1076
1077    #[test]
1078    fn test_legacy_get_with_limit() {
1079        let cmd = parse("get::users:@*[lim=10]").unwrap();
1080        assert_eq!(cmd.cages.len(), 1);
1081        assert_eq!(cmd.cages[0].kind, CageKind::Limit(10));
1082    }
1083
1084    #[test]
1085    fn test_legacy_get_with_sort_desc() {
1086        let cmd = parse("get::users:@*[^!created_at]").unwrap();
1087        assert_eq!(cmd.cages.len(), 1);
1088        assert_eq!(cmd.cages[0].kind, CageKind::Sort(SortOrder::Desc));
1089    }
1090
1091    #[test]
1092    fn test_set_command() {
1093        let cmd = parse("set::users:[verified=true][id=$1]").unwrap();
1094        assert_eq!(cmd.action, Action::Set);
1095        assert_eq!(cmd.table, "users");
1096        assert_eq!(cmd.cages.len(), 2);
1097    }
1098
1099    #[test]
1100    fn test_del_command() {
1101        let cmd = parse("del::sessions:[expired_at<now]").unwrap();
1102        assert_eq!(cmd.action, Action::Del);
1103        assert_eq!(cmd.table, "sessions");
1104    }
1105
1106    #[test]
1107    fn test_legacy_fuzzy_match() {
1108        let cmd = parse("get::users:@*[name~$1]").unwrap();
1109        assert_eq!(cmd.cages[0].conditions[0].op, Operator::Fuzzy);
1110    }
1111
1112    #[test]
1113    fn test_legacy_complex_query() {
1114        let cmd = parse("get::users:@id@email@role[active=true][lim=10]").unwrap();
1115        assert_eq!(cmd.action, Action::Get);
1116        assert_eq!(cmd.table, "users");
1117        assert_eq!(cmd.columns.len(), 3);
1118        assert_eq!(cmd.cages.len(), 2);
1119    }
1120
1121    #[test]
1122    fn test_legacy_param_in_filter() {
1123        let cmd = parse("get::users:@*[id=$1]").unwrap();
1124        assert_eq!(cmd.cages.len(), 1);
1125        assert_eq!(cmd.cages[0].conditions[0].value, Value::Param(1));
1126    }
1127
1128    #[test]
1129    fn test_legacy_param_in_update() {
1130        let cmd = parse("set::users:[verified=true][id=$1]").unwrap();
1131        assert_eq!(cmd.action, Action::Set);
1132        assert_eq!(cmd.cages.len(), 2);
1133        assert_eq!(cmd.cages[1].conditions[0].value, Value::Param(1));
1134    }
1135
1136    // ========================================================================
1137    // Schema v0.7.0 Tests (DEFAULT, CHECK)
1138    // ========================================================================
1139
1140    #[test]
1141    fn test_make_with_default_uuid() {
1142        let cmd = parse("make::users:'id:uuid^pk = uuid()").unwrap();
1143        assert_eq!(cmd.action, Action::Make);
1144        assert_eq!(cmd.table, "users");
1145        assert_eq!(cmd.columns.len(), 1);
1146        if let Column::Def { name, data_type, constraints } = &cmd.columns[0] {
1147            assert_eq!(name, "id");
1148            assert_eq!(data_type, "uuid");
1149            assert!(constraints.contains(&Constraint::PrimaryKey));
1150            assert!(constraints.iter().any(|c| matches!(c, Constraint::Default(v) if v == "uuid()")));
1151        } else {
1152            panic!("Expected Column::Def");
1153        }
1154    }
1155
1156    #[test]
1157    fn test_make_with_default_numeric() {
1158        let cmd = parse("make::stats:'count:bigint = 0").unwrap();
1159        assert_eq!(cmd.action, Action::Make);
1160        if let Column::Def { constraints, .. } = &cmd.columns[0] {
1161            assert!(constraints.iter().any(|c| matches!(c, Constraint::Default(v) if v == "0")));
1162        } else {
1163            panic!("Expected Column::Def");
1164        }
1165    }
1166
1167    #[test]
1168    fn test_make_with_check_constraint() {
1169        let cmd = parse(r#"make::orders:'status:varchar^check("pending","paid","cancelled")"#).unwrap();
1170        assert_eq!(cmd.action, Action::Make);
1171        if let Column::Def { name, constraints, .. } = &cmd.columns[0] {
1172            assert_eq!(name, "status");
1173            let check = constraints.iter().find(|c| matches!(c, Constraint::Check(_)));
1174            assert!(check.is_some());
1175            if let Some(Constraint::Check(vals)) = check {
1176                assert_eq!(vals, &vec!["pending".to_string(), "paid".to_string(), "cancelled".to_string()]);
1177            }
1178        } else {
1179            panic!("Expected Column::Def");
1180        }
1181    }
1182
1183    // ========================================================================
1184    // INDEX Tests (v0.7.0)
1185    // ========================================================================
1186
1187    #[test]
1188    fn test_index_basic() {
1189        let cmd = parse("index::idx_users_email^on(users:'email)").unwrap();
1190        assert_eq!(cmd.action, Action::Index);
1191        let idx = cmd.index_def.expect("index_def should be Some");
1192        assert_eq!(idx.name, "idx_users_email");
1193        assert_eq!(idx.table, "users");
1194        assert_eq!(idx.columns, vec!["email".to_string()]);
1195        assert!(!idx.unique);
1196    }
1197
1198    #[test]
1199    fn test_index_composite() {
1200        let cmd = parse("index::idx_lookup^on(orders:'user_id-created_at)").unwrap();
1201        assert_eq!(cmd.action, Action::Index);
1202        let idx = cmd.index_def.expect("index_def should be Some");
1203        assert_eq!(idx.name, "idx_lookup");
1204        assert_eq!(idx.table, "orders");
1205        assert_eq!(idx.columns, vec!["user_id".to_string(), "created_at".to_string()]);
1206    }
1207
1208    #[test]
1209    fn test_index_unique() {
1210        let cmd = parse("index::idx_phone^on(users:'phone)^unique").unwrap();
1211        assert_eq!(cmd.action, Action::Index);
1212        let idx = cmd.index_def.expect("index_def should be Some");
1213        assert_eq!(idx.name, "idx_phone");
1214        assert!(idx.unique);
1215    }
1216
1217    // ========================================================================
1218    // Composite Table Constraints Tests (v0.7.0)
1219    // ========================================================================
1220
1221    #[test]
1222    fn test_make_composite_unique() {
1223        let cmd = parse("make::bookings:'user_id:uuid'schedule_id:uuid^unique(user_id, schedule_id)").unwrap();
1224        assert_eq!(cmd.action, Action::Make);
1225        assert_eq!(cmd.table_constraints.len(), 1);
1226        if let TableConstraint::Unique(cols) = &cmd.table_constraints[0] {
1227            assert_eq!(cols, &vec!["user_id".to_string(), "schedule_id".to_string()]);
1228        } else {
1229            panic!("Expected TableConstraint::Unique");
1230        }
1231    }
1232
1233    #[test]
1234    fn test_make_composite_pk() {
1235        let cmd = parse("make::order_items:'order_id:uuid'product_id:uuid^pk(order_id, product_id)").unwrap();
1236        assert_eq!(cmd.action, Action::Make);
1237        assert_eq!(cmd.table_constraints.len(), 1);
1238        if let TableConstraint::PrimaryKey(cols) = &cmd.table_constraints[0] {
1239            assert_eq!(cols, &vec!["order_id".to_string(), "product_id".to_string()]);
1240        } else {
1241            panic!("Expected TableConstraint::PrimaryKey");
1242        }
1243    }
1244}