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            where_conditions: Vec::new(),
142        },
143    ))
144}
145
146/// Parse: nothing
147fn parse_conflict_nothing(input: &str) -> IResult<&str, ConflictAction> {
148    use nom::combinator::value;
149    value(ConflictAction::DoNothing, tag_no_case("nothing")).parse(input)
150}
151
152/// Parse: update col = val, col2 = val2
153fn parse_conflict_update(input: &str) -> IResult<&str, ConflictAction> {
154    let (input, _) = tag_no_case("update").parse(input)?;
155    let (input, _) = multispace1(input)?;
156    let (input, assignments) = parse_conflict_assignments(input)?;
157
158    Ok((input, ConflictAction::DoUpdate { assignments }))
159}
160
161/// Parse assignments for ON CONFLICT UPDATE: col = val, col2 = excluded.col2
162fn parse_conflict_assignments(input: &str) -> IResult<&str, Vec<(String, Expr)>> {
163    let (remaining, assignments) = separated_list1(
164        (multispace0, char(','), multispace0),
165        parse_conflict_assignment,
166    )
167    .parse(input)?;
168    if !conflict_assignment_targets_are_unique(&assignments) {
169        return Err(duplicate_target_error(input));
170    }
171    Ok((remaining, assignments))
172}
173
174/// Parse single conflict assignment: column = expression (supports :named_params)
175fn parse_conflict_assignment(input: &str) -> IResult<&str, (String, Expr)> {
176    use super::expressions::parse_expression;
177    use nom::branch::alt;
178
179    let (input, column) = parse_bare_identifier(input)?;
180    let (input, _) = multispace0(input)?;
181    let (input, _) = char('=').parse(input)?;
182    let (input, _) = multispace0(input)?;
183
184    // Try to parse a value first (handles :named_params, literals, etc.)
185    // Then fall back to full expression parsing
186    let (input, expr) = alt((
187        nom::combinator::map(parse_value, super::expressions::value_to_expr),
188        // Fall back to full expression parsing
189        parse_expression,
190    ))
191    .parse(input)?;
192
193    Ok((input, (column.to_string(), expr)))
194}
195
196fn condition_targets_are_unique(conditions: &[Condition]) -> bool {
197    let mut seen = HashSet::new();
198    conditions.iter().all(|condition| match &condition.left {
199        Expr::Named(name) => seen.insert(name.as_str()),
200        _ => true,
201    })
202}
203
204fn identifier_targets_are_unique(columns: &[&str]) -> bool {
205    let mut seen = HashSet::new();
206    columns.iter().all(|column| seen.insert(*column))
207}
208
209fn conflict_assignment_targets_are_unique(assignments: &[(String, Expr)]) -> bool {
210    let mut seen = HashSet::new();
211    assignments
212        .iter()
213        .all(|(column, _)| seen.insert(column.as_str()))
214}
215
216fn duplicate_target_error(input: &str) -> nom::Err<nom::error::Error<&str>> {
217    nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Verify))
218}
219
220/// Parse: from (get ...) - source query for INSERT...SELECT
221/// Syntax: `from (get table fields col1, col2 where ...)`
222pub fn parse_source_query(input: &str) -> IResult<&str, Box<crate::ast::Qail>> {
223    let (input, _) = multispace0(input)?;
224    let (input, _) = tag_no_case("from").parse(input)?;
225    let (input, _) = multispace0(input)?;
226    let (input, _) = char('(').parse(input)?;
227    let (input, _) = multispace0(input)?;
228    let (input, subquery) = super::parse_root(input)?;
229    let (input, _) = multispace0(input)?;
230    let (input, _) = char(')').parse(input)?;
231    Ok((input, Box::new(subquery)))
232}