qail_core/parser/grammar/
dml.rs

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