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