qail_core/parser/grammar/
dml.rs

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