qail_core/parser/grammar/
clauses.rs

1use super::base::{parse_identifier, parse_operator, parse_value};
2use super::expressions::parse_expression;
3use crate::ast::*;
4use nom::{
5    IResult, Parser,
6    branch::alt,
7    bytes::complete::tag_no_case,
8    character::complete::{char, digit1, multispace0, multispace1},
9    combinator::{map, opt, value},
10    multi::{many0, separated_list0, separated_list1},
11    sequence::{delimited, preceded},
12};
13
14/// Parse: fields id, name, email  OR  fields *
15pub fn parse_fields_clause(input: &str) -> IResult<&str, Vec<Expr>> {
16    let (input, _) = tag_no_case("fields").parse(input)?;
17    let (input, _) = multispace1(input)?;
18
19    alt((
20        // Wildcard: *
21        map(char('*'), |_| vec![Expr::Star]),
22        parse_column_list,
23    ))
24    .parse(input)
25}
26
27/// Parse comma-separated column list, respecting parenthesis depth
28/// (commas inside parens don't split columns)
29pub fn parse_column_list(input: &str) -> IResult<&str, Vec<Expr>> {
30    let mut columns = Vec::new();
31    let mut current_input = input;
32
33    loop {
34        let (remaining, col) = parse_single_column(current_input)?;
35        columns.push(col);
36        current_input = remaining;
37
38        // Skip whitespace
39        let (remaining, _) = multispace0(current_input)?;
40
41        if remaining.starts_with(',') {
42            // Consume comma and whitespace
43            let (remaining, _) = char(',')(remaining)?;
44            let (remaining, _) = multispace0(remaining)?;
45            current_input = remaining;
46        } else {
47            // No more columns
48            current_input = remaining;
49            break;
50        }
51    }
52
53    if columns.is_empty() {
54        Err(nom::Err::Error(nom::error::Error::new(
55            input,
56            nom::error::ErrorKind::SeparatedList,
57        )))
58    } else {
59        Ok((current_input, columns))
60    }
61}
62
63/// Parse a single column with optional alias: name as display_name
64pub fn parse_single_column(input: &str) -> IResult<&str, Expr> {
65    let (input, mut expr) = parse_expression(input)?;
66    let (input, _) = multispace0(input)?;
67
68    let (input, alias) =
69        opt(preceded((tag_no_case("as"), multispace1), parse_identifier)).parse(input)?;
70
71    if let Some(a) = alias {
72        // Wrap whatever expr we found in Aliased?
73        // Wait, Expr::Aliased has { name: String, alias: String }.
74        // This suggests only named columns can be aliased?
75        // AST needs update if we want aliased complex expressions.
76        // Actually Expr definition:
77        // Aliased { name: String, alias: String }
78        // Case { ..., alias: Option<String> }
79        // JsonAccess { ..., alias: Option<String> }
80        // FunctionCall { ..., alias: Option<String> }
81
82        // We should move Alias into global Expr wrapper or update Expr structure.
83        // For now, let's map what we can.
84        expr = match expr {
85            Expr::Named(n) => Expr::Aliased {
86                name: n,
87                alias: a.to_string(),
88            },
89            Expr::FunctionCall { name, args, .. } => Expr::FunctionCall {
90                name,
91                args,
92                alias: Some(a.to_string()),
93            },
94            Expr::JsonAccess {
95                column,
96                path_segments,
97                ..
98            } => Expr::JsonAccess {
99                column,
100                path_segments,
101                alias: Some(a.to_string()),
102            },
103            Expr::Case {
104                when_clauses,
105                else_value,
106                ..
107            } => Expr::Case {
108                when_clauses,
109                else_value,
110                alias: Some(a.to_string()),
111            },
112            Expr::Aggregate {
113                col,
114                func,
115                distinct,
116                filter,
117                ..
118            } => Expr::Aggregate {
119                col,
120                func,
121                distinct,
122                filter,
123                alias: Some(a.to_string()),
124            },
125            Expr::Cast {
126                expr: inner,
127                target_type,
128                ..
129            } => Expr::Cast {
130                expr: inner,
131                target_type,
132                alias: Some(a.to_string()),
133            },
134            _ => expr,
135        };
136    }
137
138    Ok((input, expr))
139}
140
141/// Parse: where col = value and col2 = value2
142pub fn parse_where_clause(input: &str) -> IResult<&str, Vec<Cage>> {
143    let (input, _) = tag_no_case("where").parse(input)?;
144    let (input, _) = multispace1(input)?;
145
146    let (input, conditions) = parse_conditions(input)?;
147
148    Ok((
149        input,
150        vec![Cage {
151            kind: CageKind::Filter,
152            conditions,
153            logical_op: LogicalOp::And, // Default, actual logic handled in conditions
154        }],
155    ))
156}
157
158/// Parse: having condition and condition2
159/// HAVING is for filtering on aggregates after GROUP BY
160pub fn parse_having_clause(input: &str) -> IResult<&str, Vec<Condition>> {
161    let (input, _) = tag_no_case("having").parse(input)?;
162    let (input, _) = multispace1(input)?;
163
164    let (input, conditions) = parse_conditions(input)?;
165
166    Ok((input, conditions))
167}
168
169/// Parse conditions with and/or
170pub fn parse_conditions(input: &str) -> IResult<&str, Vec<Condition>> {
171    let (input, first) = parse_condition(input)?;
172    // Use multispace0 before and/or to handle IS NULL case (trailing space already consumed)
173    let (input, rest) = many0(preceded(
174        (
175            multispace0,
176            alt((tag_no_case("and"), tag_no_case("or"))),
177            multispace1,
178        ),
179        parse_condition,
180    ))
181    .parse(input)?;
182
183    let mut conditions = vec![first];
184    conditions.extend(rest);
185    Ok((input, conditions))
186}
187
188/// Parse single condition: column op value OR exists (subquery) OR not exists (subquery)
189pub fn parse_condition(input: &str) -> IResult<&str, Condition> {
190    // Special case: EXISTS (subquery) and NOT EXISTS (subquery) - unary operators
191    if let Ok((input, _)) = tag_no_case::<_, _, nom::error::Error<&str>>("not exists")(input) {
192        let (input, _) = multispace0(input)?;
193        let (input, _) = char('(').parse(input)?;
194        let (input, _) = multispace0(input)?;
195        let (input, subquery) = super::parse_root(input)?;
196        let (input, _) = multispace0(input)?;
197        let (input, _) = char(')').parse(input)?;
198        return Ok((
199            input,
200            Condition {
201                left: Expr::Named("".to_string()),
202                op: Operator::NotExists,
203                value: Value::Subquery(Box::new(subquery)),
204                is_array_unnest: false,
205            },
206        ));
207    }
208    if let Ok((input, _)) = tag_no_case::<_, _, nom::error::Error<&str>>("exists")(input) {
209        let (input, _) = multispace0(input)?;
210        let (input, _) = char('(').parse(input)?;
211        let (input, _) = multispace0(input)?;
212        let (input, subquery) = super::parse_root(input)?;
213        let (input, _) = multispace0(input)?;
214        let (input, _) = char(')').parse(input)?;
215        return Ok((
216            input,
217            Condition {
218                left: Expr::Named("".to_string()),
219                op: Operator::Exists,
220                value: Value::Subquery(Box::new(subquery)),
221                is_array_unnest: false,
222            },
223        ));
224    }
225
226    // Normal case: column op value
227    let (input, left_expr) = parse_expression(input)?;
228    let (input, _) = multispace0(input)?;
229
230    let (input, op) = parse_operator(input)?;
231    let (input, _) = multispace0(input)?;
232
233    let (input, value) = if matches!(op, Operator::IsNull | Operator::IsNotNull) {
234        (input, Value::Null)
235    } else if matches!(op, Operator::Between | Operator::NotBetween) {
236        let (input, min_val) = parse_value(input)?;
237        let (input, _) = multispace1(input)?;
238        let (input, _) = tag_no_case("and").parse(input)?;
239        let (input, _) = multispace1(input)?;
240        let (input, max_val) = parse_value(input)?;
241        // Store as array with 2 elements [min, max]
242        (input, Value::Array(vec![min_val, max_val]))
243    } else if matches!(op, Operator::In | Operator::NotIn) {
244        let (input, _) = char('(').parse(input)?;
245        let (input, _) = multispace0(input)?;
246        let (input, values) =
247            separated_list0((multispace0, char(','), multispace0), parse_value).parse(input)?;
248        let (input, _) = multispace0(input)?;
249        let (input, _) = char(')').parse(input)?;
250        (input, Value::Array(values))
251    } else if let Ok((i, val)) = parse_value(input) {
252        (i, val)
253    } else {
254        let (i, col_name) = parse_identifier(input)?;
255        (i, Value::Column(col_name.to_string()))
256    };
257
258    Ok((
259        input,
260        Condition {
261            left: left_expr,
262            op,
263            value,
264            is_array_unnest: false,
265        },
266    ))
267}
268
269/// Parse: order by col [asc|desc], col2 [asc|desc]
270pub fn parse_order_by_clause(input: &str) -> IResult<&str, Vec<Cage>> {
271    let (input, _) = tag_no_case("order").parse(input)?;
272    let (input, _) = multispace1(input)?;
273    let (input, _) = tag_no_case("by").parse(input)?;
274    let (input, _) = multispace1(input)?;
275
276    let (input, sorts) =
277        separated_list1((multispace0, char(','), multispace0), parse_sort_column).parse(input)?;
278
279    Ok((input, sorts))
280}
281
282/// Parse single sort column: col [asc|desc]
283pub fn parse_sort_column(input: &str) -> IResult<&str, Cage> {
284    let (input, expr) = parse_expression(input)?;
285    let (input, _) = multispace0(input)?;
286
287    let (input, order) = opt(alt((
288        value(SortOrder::Desc, tag_no_case("desc")),
289        value(SortOrder::Asc, tag_no_case("asc")),
290    )))
291    .parse(input)?;
292
293    Ok((
294        input,
295        Cage {
296            kind: CageKind::Sort(order.unwrap_or(SortOrder::Asc)),
297            conditions: vec![Condition {
298                left: expr,
299                op: Operator::Eq,
300                value: Value::Null,
301                is_array_unnest: false,
302            }],
303            logical_op: LogicalOp::And,
304        },
305    ))
306}
307
308/// Parse: limit N
309pub fn parse_limit_clause(input: &str) -> IResult<&str, Cage> {
310    let (input, _) = tag_no_case("limit").parse(input)?;
311    let (input, _) = multispace1(input)?;
312    let (input, n) = digit1(input)?;
313
314    Ok((
315        input,
316        Cage {
317            kind: CageKind::Limit(n.parse().unwrap_or(0)),
318            conditions: vec![],
319            logical_op: LogicalOp::And,
320        },
321    ))
322}
323
324/// Parse: offset N
325pub fn parse_offset_clause(input: &str) -> IResult<&str, Cage> {
326    let (input, _) = tag_no_case("offset").parse(input)?;
327    let (input, _) = multispace1(input)?;
328    let (input, n) = digit1(input)?;
329
330    Ok((
331        input,
332        Cage {
333            kind: CageKind::Offset(n.parse().unwrap_or(0)),
334            conditions: vec![],
335            logical_op: LogicalOp::And,
336        },
337    ))
338}
339
340/// Parse: DISTINCT ON (col1, col2, ...)
341/// Returns list of column names for DISTINCT ON
342pub fn parse_distinct_on(input: &str) -> IResult<&str, Vec<String>> {
343    let (input, _) = tag_no_case("distinct").parse(input)?;
344    let (input, _) = multispace1(input)?;
345    let (input, _) = tag_no_case("on").parse(input)?;
346    let (input, _) = multispace0(input)?;
347
348    let (input, cols) = delimited(
349        char('('),
350        separated_list1(
351            (multispace0, char(','), multispace0),
352            map(parse_identifier, |s| s.to_string()),
353        ),
354        char(')'),
355    )
356    .parse(input)?;
357
358    Ok((input, cols))
359}