Skip to main content

qail_core/parser/grammar/
dml.rs

1use super::base::{parse_bare_identifier, parse_value};
2use crate::ast::*;
3use nom::{
4    IResult, Parser,
5    bytes::complete::tag_no_case,
6    character::complete::{char, multispace0, multispace1},
7    multi::separated_list1,
8};
9use std::collections::HashSet;
10
11/// Parse: values col = val, col2 = val2 (for SET/UPDATE)
12pub fn parse_values_clause(input: &str) -> IResult<&str, Cage> {
13    let (input, _) = tag_no_case("values").parse(input)?;
14    let (input, _) = multispace1(input)?;
15
16    let (input, conditions) = parse_set_assignments(input)?;
17
18    Ok((
19        input,
20        Cage {
21            kind: CageKind::Payload,
22            conditions,
23            logical_op: LogicalOp::And,
24        },
25    ))
26}
27
28/// Parse: values :val1, :val2 (for INSERT/ADD) - just list of values without column names
29pub fn parse_insert_values(input: &str) -> IResult<&str, Cage> {
30    let (input, _) = tag_no_case("values").parse(input)?;
31    let (input, _) = multispace1(input)?;
32
33    let (input, values) =
34        separated_list1((multispace0, char(','), multispace0), parse_value).parse(input)?;
35
36    let conditions: Vec<Condition> = values
37        .into_iter()
38        .enumerate()
39        .map(|(i, val)| {
40            Condition {
41                left: Expr::Named(format!("${}", i + 1)), // Use positional placeholder for column
42                op: Operator::Eq,
43                value: val,
44                is_array_unnest: false,
45            }
46        })
47        .collect();
48
49    Ok((
50        input,
51        Cage {
52            kind: CageKind::Payload,
53            conditions,
54            logical_op: LogicalOp::And,
55        },
56    ))
57}
58
59/// Parse comma-separated assignments: col = val, col2 = val2
60pub fn parse_set_assignments(input: &str) -> IResult<&str, Vec<Condition>> {
61    let (remaining, conditions) =
62        separated_list1((multispace0, char(','), multispace0), parse_assignment).parse(input)?;
63    if !condition_targets_are_unique(&conditions) {
64        return Err(duplicate_target_error(input));
65    }
66    Ok((remaining, conditions))
67}
68
69/// Parse single assignment: column = value or column = expression (supports functions and subqueries)
70pub fn parse_assignment(input: &str) -> IResult<&str, Condition> {
71    use super::expressions::parse_expression;
72    use nom::branch::alt;
73
74    let (input, column) = parse_bare_identifier(input)?;
75    let (input, _) = multispace0(input)?;
76    let (input, _) = char('=').parse(input)?;
77    let (input, _) = multispace0(input)?;
78
79    // Try simple value first (booleans, strings, numbers, params), then subquery, then expression
80    let (input, value) = alt((
81        // Try simple value parsing first (handles booleans, strings, numbers, params)
82        parse_value,
83        // Try parenthesized subquery: (get ...)
84        parse_subquery_value,
85        // Fall back to expression and convert to Value::Function
86        nom::combinator::map(parse_expression, |expr| Value::Function(expr.to_string())),
87    ))
88    .parse(input)?;
89
90    Ok((
91        input,
92        Condition {
93            left: Expr::Named(column.to_string()),
94            op: Operator::Eq,
95            value,
96            is_array_unnest: false,
97        },
98    ))
99}
100
101/// Parse a subquery value: (get ...) -> Value::Subquery
102fn parse_subquery_value(input: &str) -> IResult<&str, Value> {
103    let (input, _) = char('(').parse(input)?;
104    let (input, _) = multispace0(input)?;
105    let (input, subquery) = super::parse_root(input)?;
106    let (input, _) = multispace0(input)?;
107    let (input, _) = char(')').parse(input)?;
108    Ok((input, Value::Subquery(Box::new(subquery))))
109}
110
111/// Parse ON CONFLICT clause: conflict (col1, col2) update col = val OR conflict (col) nothing
112/// Syntax:
113/// - `conflict (col1, col2) nothing` -> ON CONFLICT (col1, col2) DO NOTHING
114/// - `conflict (col1) update col2 = val` -> ON CONFLICT (col1) DO UPDATE SET col2 = val
115pub fn parse_on_conflict(input: &str) -> IResult<&str, OnConflict> {
116    use nom::branch::alt;
117
118    let (input, _) = multispace0(input)?;
119    let (input, _) = tag_no_case("conflict").parse(input)?;
120    let (input, _) = multispace0(input)?;
121
122    let (input, _) = char('(').parse(input)?;
123    let (input, _) = multispace0(input)?;
124    let (input, columns) =
125        separated_list1((multispace0, char(','), multispace0), parse_bare_identifier)
126            .parse(input)?;
127    if !identifier_targets_are_unique(&columns) {
128        return Err(duplicate_target_error(input));
129    }
130    let (input, _) = multispace0(input)?;
131    let (input, _) = char(')').parse(input)?;
132    let (input, _) = multispace0(input)?;
133
134    let (input, action) = alt((parse_conflict_nothing, parse_conflict_update)).parse(input)?;
135
136    Ok((
137        input,
138        OnConflict {
139            columns: columns.iter().map(|s| s.to_string()).collect(),
140            action,
141        },
142    ))
143}
144
145/// Parse: nothing
146fn parse_conflict_nothing(input: &str) -> IResult<&str, ConflictAction> {
147    use nom::combinator::value;
148    value(ConflictAction::DoNothing, tag_no_case("nothing")).parse(input)
149}
150
151/// Parse: update col = val, col2 = val2
152fn parse_conflict_update(input: &str) -> IResult<&str, ConflictAction> {
153    let (input, _) = tag_no_case("update").parse(input)?;
154    let (input, _) = multispace1(input)?;
155    let (input, assignments) = parse_conflict_assignments(input)?;
156
157    Ok((input, ConflictAction::DoUpdate { assignments }))
158}
159
160/// Parse assignments for ON CONFLICT UPDATE: col = val, col2 = excluded.col2
161fn parse_conflict_assignments(input: &str) -> IResult<&str, Vec<(String, Expr)>> {
162    let (remaining, assignments) = separated_list1(
163        (multispace0, char(','), multispace0),
164        parse_conflict_assignment,
165    )
166    .parse(input)?;
167    if !conflict_assignment_targets_are_unique(&assignments) {
168        return Err(duplicate_target_error(input));
169    }
170    Ok((remaining, assignments))
171}
172
173/// Parse single conflict assignment: column = expression (supports :named_params)
174fn parse_conflict_assignment(input: &str) -> IResult<&str, (String, Expr)> {
175    use super::expressions::parse_expression;
176    use nom::branch::alt;
177
178    let (input, column) = parse_bare_identifier(input)?;
179    let (input, _) = multispace0(input)?;
180    let (input, _) = char('=').parse(input)?;
181    let (input, _) = multispace0(input)?;
182
183    // Try to parse a value first (handles :named_params, literals, etc.)
184    // Then fall back to full expression parsing
185    let (input, expr) = alt((
186        nom::combinator::map(parse_value, super::expressions::value_to_expr),
187        // Fall back to full expression parsing
188        parse_expression,
189    ))
190    .parse(input)?;
191
192    Ok((input, (column.to_string(), expr)))
193}
194
195fn condition_targets_are_unique(conditions: &[Condition]) -> bool {
196    let mut seen = HashSet::new();
197    conditions.iter().all(|condition| match &condition.left {
198        Expr::Named(name) => seen.insert(name.as_str()),
199        _ => true,
200    })
201}
202
203fn identifier_targets_are_unique(columns: &[&str]) -> bool {
204    let mut seen = HashSet::new();
205    columns.iter().all(|column| seen.insert(*column))
206}
207
208fn conflict_assignment_targets_are_unique(assignments: &[(String, Expr)]) -> bool {
209    let mut seen = HashSet::new();
210    assignments
211        .iter()
212        .all(|(column, _)| seen.insert(column.as_str()))
213}
214
215fn duplicate_target_error(input: &str) -> nom::Err<nom::error::Error<&str>> {
216    nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Verify))
217}
218
219/// Parse: from (get ...) - source query for INSERT...SELECT
220/// Syntax: `from (get table fields col1, col2 where ...)`
221pub fn parse_source_query(input: &str) -> IResult<&str, Box<crate::ast::Qail>> {
222    let (input, _) = multispace0(input)?;
223    let (input, _) = tag_no_case("from").parse(input)?;
224    let (input, _) = multispace0(input)?;
225    let (input, _) = char('(').parse(input)?;
226    let (input, _) = multispace0(input)?;
227    let (input, subquery) = super::parse_root(input)?;
228    let (input, _) = multispace0(input)?;
229    let (input, _) = char(')').parse(input)?;
230    Ok((input, Box::new(subquery)))
231}