qail_core/parser/grammar/
mod.rs

1pub mod base;
2pub mod binary_ops;
3pub mod case_when;
4pub mod clauses;
5pub mod cte;
6pub mod ddl;
7pub mod dml;
8pub mod expressions;
9pub mod functions;
10pub mod joins;
11pub mod special_funcs;
12
13use crate::ast::*;
14use nom::{
15    IResult, Parser,
16    bytes::complete::tag_no_case,
17    character::complete::{multispace0, multispace1},
18    combinator::opt,
19    multi::many0,
20};
21use self::base::*;
22use self::clauses::*;
23use self::ddl::*;
24use self::dml::*;
25use self::joins::*;
26// use self::expressions::*; // Used in clauses module
27
28/// Parse a QAIL query with comment preprocessing.
29/// This is the recommended entry point - handles SQL comment stripping.
30pub fn parse(input: &str) -> Result<Qail, String> {
31    let cleaned = strip_sql_comments(input);
32    match parse_root(&cleaned) {
33        Ok((_, cmd)) => Ok(cmd),
34        Err(e) => Err(format!("Parse error: {:?}", e)),
35    }
36}
37
38/// Parse a QAIL query (root entry point).
39/// Note: Does NOT strip comments. Use `parse()` for automatic comment handling.
40pub fn parse_root(input: &str) -> IResult<&str, Qail> {
41    let input = input.trim();
42
43    // Try transaction commands first (single keywords)
44    if let Ok((remaining, cmd)) = parse_txn_command(input) {
45        return Ok((remaining, cmd));
46    }
47
48    // Try CREATE INDEX first (special case: "index name on table ...")
49    if let Ok((remaining, cmd)) = parse_create_index(input) {
50        return Ok((remaining, cmd));
51    }
52
53    // Try WITH clause (CTE) parsing
54    let lower_input = input.to_lowercase();
55    let (input, ctes) = if lower_input.starts_with("with")
56        && lower_input
57            .chars()
58            .nth(4)
59            .map(|c| c.is_whitespace())
60            .unwrap_or(false)
61    {
62        let (remaining, (cte_defs, _is_recursive)) = cte::parse_with_clause(input)?;
63        let (remaining, _) = multispace0(remaining)?;
64        (remaining, cte_defs)
65    } else {
66        (input, vec![])
67    };
68
69    let (input, (action, distinct)) = parse_action(input)?;
70    // v2 syntax only: whitespace separator between action and table
71    let (input, _) = multispace1(input)?;
72
73    // Supports expressions like: CASE WHEN ... END, functions, columns
74    let (input, distinct_on) = if distinct {
75        // If already parsed "get distinct", check for "on (...)"
76        if let Ok((remaining, _)) = tag_no_case::<_, _, nom::error::Error<&str>>("on").parse(input)
77        {
78            let (remaining, _) = multispace0(remaining)?;
79            let (remaining, exprs) = nom::sequence::delimited(
80                nom::character::complete::char('('),
81                nom::multi::separated_list1(
82                    (
83                        multispace0,
84                        nom::character::complete::char(','),
85                        multispace0,
86                    ),
87                    expressions::parse_expression,
88                ),
89                nom::character::complete::char(')'),
90            )
91            .parse(remaining)?;
92            let (remaining, _) = multispace1(remaining)?;
93            (remaining, exprs)
94        } else {
95            (input, vec![])
96        }
97    } else {
98        (input, vec![])
99    };
100
101    //  Parse table name
102    let (input, table) = parse_identifier(input)?;
103    let (input, _) = multispace0(input)?;
104
105    // For MAKE (CREATE TABLE): parse column definitions
106    if matches!(action, Action::Make) {
107        return parse_create_table(input, table);
108    }
109
110    let (input, joins) = many0(parse_join_clause).parse(input)?;
111    let (input, _) = multispace0(input)?;
112
113    // For SET/UPDATE: parse "values col = val, col2 = val2" before fields
114    let (input, set_cages) = if matches!(action, Action::Set) {
115        opt(parse_values_clause).parse(input)?
116    } else {
117        (input, None)
118    };
119    let (input, _) = multispace0(input)?;
120
121    let (input, columns) = opt(parse_fields_clause).parse(input)?;
122    let (input, _) = multispace0(input)?;
123
124    // For ADD/INSERT: try "from (get ...)" first, then fall back to "values val1, val2"
125    let (input, source_query) = if matches!(action, Action::Add) {
126        opt(dml::parse_source_query).parse(input)?
127    } else {
128        (input, None)
129    };
130    let (input, _) = multispace0(input)?;
131
132    // Only parse values if no source_query (INSERT...SELECT takes precedence)
133    let (input, add_cages) = if source_query.is_none() && matches!(action, Action::Add) {
134        opt(dml::parse_insert_values).parse(input)?
135    } else {
136        (input, None)
137    };
138    let (input, _) = multispace0(input)?;
139
140    let (input, where_cages) = opt(parse_where_clause).parse(input)?;
141    let (input, _) = multispace0(input)?;
142
143    let (input, having) = opt(parse_having_clause).parse(input)?;
144    let (input, _) = multispace0(input)?;
145
146    let (input, on_conflict) = if matches!(action, Action::Add) {
147        opt(dml::parse_on_conflict).parse(input)?
148    } else {
149        (input, None)
150    };
151    let (input, _) = multispace0(input)?;
152
153    let (input, order_cages) = opt(parse_order_by_clause).parse(input)?;
154    let (input, _) = multispace0(input)?;
155    let (input, limit_cage) = opt(parse_limit_clause).parse(input)?;
156    let (input, _) = multispace0(input)?;
157    let (input, offset_cage) = opt(parse_offset_clause).parse(input)?;
158
159    let mut cages = Vec::new();
160
161    // For SET, values come first (as Payload cage)
162    if let Some(sc) = set_cages {
163        cages.push(sc);
164    }
165
166    // For ADD, values come as Payload cage too
167    if let Some(ac) = add_cages {
168        cages.push(ac);
169    }
170
171    if let Some(wc) = where_cages {
172        cages.extend(wc);
173    }
174    if let Some(oc) = order_cages {
175        cages.extend(oc);
176    }
177    if let Some(lc) = limit_cage {
178        cages.push(lc);
179    }
180    if let Some(oc) = offset_cage {
181        cages.push(oc);
182    }
183
184    Ok((
185        input,
186        Qail {
187            action,
188            table: table.to_string(),
189            columns: columns.unwrap_or_else(|| vec![Expr::Star]),
190            joins,
191            cages,
192            distinct,
193            distinct_on,
194            index_def: None,
195            table_constraints: vec![],
196            set_ops: vec![],
197            having: having.unwrap_or_default(),
198            group_by_mode: GroupByMode::default(),
199            returning: None,
200            ctes,
201            on_conflict,
202            source_query,
203            channel: None,
204            payload: None,
205            savepoint_name: None,
206            from_tables: vec![],
207            using_tables: vec![],
208            lock_mode: None,
209            fetch: None,
210            default_values: false,
211            overriding: None,
212            sample: None,
213            only_table: false,
214            vector: None,
215            score_threshold: None,
216            vector_name: None,
217            with_vector: false,
218            vector_size: None,
219            distance: None,
220            on_disk: None,
221            function_def: None,
222            trigger_def: None,
223            raw_value: None,
224            redis_ttl: None,
225            redis_set_condition: None,
226        },
227    ))
228}
229
230/// Strip SQL comments from input (both -- line comments and /* */ block comments)
231fn strip_sql_comments(input: &str) -> String {
232    let mut result = String::with_capacity(input.len());
233    let mut chars = input.chars().peekable();
234
235    while let Some(c) = chars.next() {
236        if c == '-' && chars.peek() == Some(&'-') {
237            // Line comment: skip until end of line
238            chars.next(); // consume second -
239            while let Some(&nc) = chars.peek() {
240                if nc == '\n' {
241                    result.push('\n'); // preserve newline
242                    chars.next();
243                    break;
244                }
245                chars.next();
246            }
247        } else if c == '/' && chars.peek() == Some(&'*') {
248            // Block comment: skip until */
249            chars.next(); // consume *
250            while let Some(nc) = chars.next() {
251                if nc == '*' && chars.peek() == Some(&'/') {
252                    chars.next(); // consume /
253                    result.push(' '); // replace with space to preserve separation
254                    break;
255                }
256            }
257        } else {
258            result.push(c);
259        }
260    }
261
262    result
263}