Skip to main content

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
30/// and `table[filter]` shorthand desugaring.
31pub fn parse(input: &str) -> Result<Qail, String> {
32    let cleaned = strip_sql_comments(input);
33    // Desugar table[filter] shorthand: "set users[active = true] fields ..."
34    // → "set users fields ... where active = true"
35    let desugared = desugar_bracket_filter(&cleaned);
36    match parse_root(&desugared) {
37        Ok(("", cmd)) => Ok(cmd),
38        Ok((remaining, _)) => Err(format!(
39            "Unexpected trailing content: '{}'", remaining
40        )),
41        Err(e) => Err(format!("Parse error: {:?}", e)),
42    }
43}
44
45/// Desugar `table[filter]` shorthand into `table ... where filter`.
46/// Transforms: `action table[cond] rest` → `action table rest where cond`
47fn desugar_bracket_filter(input: &str) -> String {
48    let trimmed = input.trim();
49    // Find the opening bracket after the table name
50    // Must be: action<ws>table[...] — the [ must immediately follow the table name
51    if let Some(bracket_start) = trimmed.find('[') {
52        // Ensure the bracket is in the table position (after action + space + identifier)
53        let before_bracket = &trimmed[..bracket_start];
54        // There should be at least "action table" before the bracket
55        if !before_bracket.contains(' ') {
56            return trimmed.to_string();
57        }
58
59        // Find matching closing bracket, respecting nesting and quotes
60        let after_bracket = &trimmed[bracket_start + 1..];
61        let mut depth = 1;
62        let mut in_single_quote = false;
63        let mut in_double_quote = false;
64        let mut bracket_end = None;
65
66        for (i, c) in after_bracket.char_indices() {
67            match c {
68                '\'' if !in_double_quote => in_single_quote = !in_single_quote,
69                '"' if !in_single_quote => in_double_quote = !in_double_quote,
70                '[' if !in_single_quote && !in_double_quote => depth += 1,
71                ']' if !in_single_quote && !in_double_quote => {
72                    depth -= 1;
73                    if depth == 0 {
74                        bracket_end = Some(i);
75                        break;
76                    }
77                }
78                _ => {}
79            }
80        }
81
82        if let Some(end_pos) = bracket_end {
83            let filter = &after_bracket[..end_pos];
84            let rest = &after_bracket[end_pos + 1..].trim();
85
86            // Check if there's already a "where" in the rest
87            let rest_lower = rest.to_lowercase();
88            if rest_lower.contains("where ") || rest_lower.contains("where\n") {
89                // Already has WHERE — append with AND
90                return format!(
91                    "{} {} AND {}",
92                    before_bracket, rest, filter
93                );
94            } else if rest.is_empty() {
95                return format!("{} where {}", before_bracket, filter);
96            } else {
97                return format!("{} {} where {}", before_bracket, rest, filter);
98            }
99        }
100    }
101    trimmed.to_string()
102}
103
104/// Parse a QAIL query (root entry point).
105/// Note: Does NOT strip comments. Use `parse()` for automatic comment handling.
106pub fn parse_root(input: &str) -> IResult<&str, Qail> {
107    let input = input.trim();
108
109    // Try transaction commands first (single keywords)
110    if let Ok((remaining, cmd)) = parse_txn_command(input) {
111        return Ok((remaining, cmd));
112    }
113
114    // Try CREATE INDEX first (special case: "index name on table ...")
115    if let Ok((remaining, cmd)) = parse_create_index(input) {
116        return Ok((remaining, cmd));
117    }
118
119    // Try WITH clause (CTE) parsing
120    let lower_input = input.to_lowercase();
121    let (input, ctes) = if lower_input.starts_with("with")
122        && lower_input
123            .chars()
124            .nth(4)
125            .map(|c| c.is_whitespace())
126            .unwrap_or(false)
127    {
128        let (remaining, (cte_defs, _is_recursive)) = cte::parse_with_clause(input)?;
129        let (remaining, _) = multispace0(remaining)?;
130        (remaining, cte_defs)
131    } else {
132        (input, vec![])
133    };
134
135    let (input, (action, distinct)) = parse_action(input)?;
136    // v2 syntax only: whitespace separator between action and table
137    let (input, _) = multispace1(input)?;
138
139    // Supports expressions like: CASE WHEN ... END, functions, columns
140    let (input, distinct_on) = if distinct {
141        // If already parsed "get distinct", check for "on (...)"
142        if let Ok((remaining, _)) = tag_no_case::<_, _, nom::error::Error<&str>>("on").parse(input)
143        {
144            let (remaining, _) = multispace0(remaining)?;
145            let (remaining, exprs) = nom::sequence::delimited(
146                nom::character::complete::char('('),
147                nom::multi::separated_list1(
148                    (
149                        multispace0,
150                        nom::character::complete::char(','),
151                        multispace0,
152                    ),
153                    expressions::parse_expression,
154                ),
155                nom::character::complete::char(')'),
156            )
157            .parse(remaining)?;
158            let (remaining, _) = multispace1(remaining)?;
159            (remaining, exprs)
160        } else {
161            (input, vec![])
162        }
163    } else {
164        (input, vec![])
165    };
166
167    //  Parse table name
168    let (input, table) = parse_identifier(input)?;
169    let (input, _) = multispace0(input)?;
170
171    // For MAKE (CREATE TABLE): parse column definitions
172    if matches!(action, Action::Make) {
173        return parse_create_table(input, table);
174    }
175
176    let (input, joins) = many0(parse_join_clause).parse(input)?;
177    let (input, _) = multispace0(input)?;
178
179    // For SET/UPDATE: parse "values col = val, col2 = val2" before fields
180    let (input, set_cages) = if matches!(action, Action::Set) {
181        opt(parse_values_clause).parse(input)?
182    } else {
183        (input, None)
184    };
185    let (input, _) = multispace0(input)?;
186
187    let (input, columns) = opt(parse_fields_clause).parse(input)?;
188    let (input, _) = multispace0(input)?;
189
190    // For ADD/INSERT: try "from (get ...)" first, then fall back to "values val1, val2"
191    let (input, source_query) = if matches!(action, Action::Add) {
192        opt(dml::parse_source_query).parse(input)?
193    } else {
194        (input, None)
195    };
196    let (input, _) = multispace0(input)?;
197
198    // Only parse values if no source_query (INSERT...SELECT takes precedence)
199    let (input, add_cages) = if source_query.is_none() && matches!(action, Action::Add) {
200        opt(dml::parse_insert_values).parse(input)?
201    } else {
202        (input, None)
203    };
204    let (input, _) = multispace0(input)?;
205
206    let (input, where_cages) = opt(parse_where_clause).parse(input)?;
207    let (input, _) = multispace0(input)?;
208
209    let (input, having) = opt(parse_having_clause).parse(input)?;
210    let (input, _) = multispace0(input)?;
211
212    let (input, on_conflict) = if matches!(action, Action::Add) {
213        opt(dml::parse_on_conflict).parse(input)?
214    } else {
215        (input, None)
216    };
217    let (input, _) = multispace0(input)?;
218
219    let (input, order_cages) = opt(parse_order_by_clause).parse(input)?;
220    let (input, _) = multispace0(input)?;
221    let (input, limit_cage) = opt(parse_limit_clause).parse(input)?;
222    let (input, _) = multispace0(input)?;
223    let (input, offset_cage) = opt(parse_offset_clause).parse(input)?;
224
225    let mut cages = Vec::new();
226
227    // For SET, values come first (as Payload cage)
228    if let Some(sc) = set_cages {
229        cages.push(sc);
230    }
231
232    // For ADD, values come as Payload cage too
233    if let Some(ac) = add_cages {
234        cages.push(ac);
235    }
236
237    if let Some(wc) = where_cages {
238        cages.extend(wc);
239    }
240    if let Some(oc) = order_cages {
241        cages.extend(oc);
242    }
243    if let Some(lc) = limit_cage {
244        cages.push(lc);
245    }
246    if let Some(oc) = offset_cage {
247        cages.push(oc);
248    }
249
250    Ok((
251        input,
252        Qail {
253            action,
254            table: table.to_string(),
255            columns: columns.unwrap_or_else(|| vec![Expr::Star]),
256            joins,
257            cages,
258            distinct,
259            distinct_on,
260            index_def: None,
261            table_constraints: vec![],
262            set_ops: vec![],
263            having: having.unwrap_or_default(),
264            group_by_mode: GroupByMode::default(),
265            returning: None,
266            ctes,
267            on_conflict,
268            source_query,
269            channel: None,
270            payload: None,
271            savepoint_name: None,
272            from_tables: vec![],
273            using_tables: vec![],
274            lock_mode: None,
275            fetch: None,
276            default_values: false,
277            overriding: None,
278            sample: None,
279            only_table: false,
280            vector: None,
281            score_threshold: None,
282            vector_name: None,
283            with_vector: false,
284            vector_size: None,
285            distance: None,
286            on_disk: None,
287            function_def: None,
288            trigger_def: None,
289            raw_value: None,
290            redis_ttl: None,
291            redis_set_condition: None,
292        },
293    ))
294}
295
296/// Strip SQL comments from input (both -- line comments and /* */ block comments)
297fn strip_sql_comments(input: &str) -> String {
298    let mut result = String::with_capacity(input.len());
299    let mut chars = input.chars().peekable();
300
301    while let Some(c) = chars.next() {
302        if c == '-' && chars.peek() == Some(&'-') {
303            // Line comment: skip until end of line
304            chars.next(); // consume second -
305            while let Some(&nc) = chars.peek() {
306                if nc == '\n' {
307                    result.push('\n'); // preserve newline
308                    chars.next();
309                    break;
310                }
311                chars.next();
312            }
313        } else if c == '/' && chars.peek() == Some(&'*') {
314            // Block comment: skip until */
315            chars.next(); // consume *
316            while let Some(nc) = chars.next() {
317                if nc == '*' && chars.peek() == Some(&'/') {
318                    chars.next(); // consume /
319                    result.push(' '); // replace with space to preserve separation
320                    break;
321                }
322            }
323        } else {
324            result.push(c);
325        }
326    }
327
328    result
329}