Skip to main content

oxigdal_query/parser/
sql.rs

1//! SQL parser implementation.
2
3use crate::error::{QueryError, Result};
4use crate::parser::ast::*;
5use sqlparser::ast as sql_ast;
6use sqlparser::dialect::GenericDialect;
7use sqlparser::parser::Parser as SqlParser;
8
9/// Parse SQL string into AST.
10pub fn parse_sql(sql: &str) -> Result<Statement> {
11    let dialect = GenericDialect {};
12    let statements = SqlParser::parse_sql(&dialect, sql)?;
13
14    if statements.is_empty() {
15        return Err(QueryError::semantic("Empty SQL statement"));
16    }
17
18    if statements.len() > 1 {
19        return Err(QueryError::semantic("Multiple statements not supported"));
20    }
21
22    convert_statement(&statements[0])
23}
24
25fn convert_statement(stmt: &sql_ast::Statement) -> Result<Statement> {
26    match stmt {
27        sql_ast::Statement::Query(query) => {
28            let select = convert_query(query)?;
29            Ok(Statement::Select(select))
30        }
31        _ => Err(QueryError::unsupported("Only SELECT statements supported")),
32    }
33}
34
35fn convert_query(query: &sql_ast::Query) -> Result<SelectStatement> {
36    if let sql_ast::SetExpr::Select(select) = &*query.body {
37        let mut stmt = SelectStatement {
38            projection: Vec::new(),
39            from: None,
40            selection: None,
41            group_by: Vec::new(),
42            having: None,
43            order_by: Vec::new(),
44            limit: None,
45            offset: None,
46        };
47
48        // Convert projection
49        for item in &select.projection {
50            stmt.projection.push(convert_select_item(item)?);
51        }
52
53        // Convert FROM clause
54        if !select.from.is_empty() {
55            stmt.from = Some(convert_table_reference(&select.from[0])?);
56        }
57
58        // Convert WHERE clause
59        if let Some(selection) = &select.selection {
60            stmt.selection = Some(convert_expr(selection)?);
61        }
62
63        // Convert GROUP BY clause
64        match &select.group_by {
65            sql_ast::GroupByExpr::Expressions(exprs, _) => {
66                for expr in exprs {
67                    stmt.group_by.push(convert_expr(expr)?);
68                }
69            }
70            sql_ast::GroupByExpr::All(_) => {
71                return Err(QueryError::unsupported("GROUP BY ALL not supported"));
72            }
73        }
74
75        // Convert HAVING clause
76        if let Some(having) = &select.having {
77            stmt.having = Some(convert_expr(having)?);
78        }
79
80        // Convert ORDER BY clause
81        if let Some(order_by) = &query.order_by {
82            if let sql_ast::OrderByKind::Expressions(exprs) = &order_by.kind {
83                for order_expr in exprs {
84                    stmt.order_by.push(convert_order_by_expr(order_expr)?);
85                }
86            }
87        }
88
89        // Convert LIMIT and OFFSET clauses
90        if let Some(limit_clause) = &query.limit_clause {
91            match limit_clause {
92                sql_ast::LimitClause::LimitOffset { limit, offset, .. } => {
93                    if let Some(limit_expr) = limit {
94                        stmt.limit = Some(convert_limit(limit_expr)?);
95                    }
96                    if let Some(offset_val) = offset {
97                        stmt.offset = Some(convert_offset(offset_val)?);
98                    }
99                }
100                sql_ast::LimitClause::OffsetCommaLimit { limit, offset } => {
101                    stmt.limit = Some(convert_limit(limit)?);
102                    stmt.offset = Some(convert_limit(offset)?);
103                }
104            }
105        }
106
107        Ok(stmt)
108    } else {
109        Err(QueryError::unsupported(
110            "Only simple SELECT queries supported",
111        ))
112    }
113}
114
115fn convert_select_item(item: &sql_ast::SelectItem) -> Result<SelectItem> {
116    match item {
117        sql_ast::SelectItem::UnnamedExpr(expr) => Ok(SelectItem::Expr {
118            expr: convert_expr(expr)?,
119            alias: None,
120        }),
121        sql_ast::SelectItem::ExprWithAlias { expr, alias } => Ok(SelectItem::Expr {
122            expr: convert_expr(expr)?,
123            alias: Some(alias.value.clone()),
124        }),
125        sql_ast::SelectItem::Wildcard(_) => Ok(SelectItem::Wildcard),
126        sql_ast::SelectItem::QualifiedWildcard(obj_name, _) => {
127            Ok(SelectItem::QualifiedWildcard(obj_name.to_string()))
128        }
129        sql_ast::SelectItem::ExprWithAliases { .. } => Err(crate::error::QueryError::Unsupported(
130            "ExprWithAliases SELECT items are not supported".into(),
131        )),
132    }
133}
134
135fn convert_table_reference(table: &sql_ast::TableWithJoins) -> Result<TableReference> {
136    let mut result = convert_table_factor(&table.relation)?;
137
138    for join in &table.joins {
139        let right = convert_table_factor(&join.relation)?;
140        let join_type = convert_join_type(&join.join_operator)?;
141        let on = match &join.join_operator {
142            sql_ast::JoinOperator::Inner(constraint)
143            | sql_ast::JoinOperator::LeftOuter(constraint)
144            | sql_ast::JoinOperator::RightOuter(constraint)
145            | sql_ast::JoinOperator::FullOuter(constraint) => convert_join_constraint(constraint)?,
146            sql_ast::JoinOperator::CrossJoin(_) => None,
147            _ => return Err(QueryError::unsupported("Unsupported join type")),
148        };
149
150        result = TableReference::Join {
151            left: Box::new(result),
152            right: Box::new(right),
153            join_type,
154            on,
155        };
156    }
157
158    Ok(result)
159}
160
161fn convert_table_factor(factor: &sql_ast::TableFactor) -> Result<TableReference> {
162    match factor {
163        sql_ast::TableFactor::Table {
164            name, alias, args, ..
165        } => {
166            if args.is_some() {
167                return Err(QueryError::unsupported("Table functions not supported"));
168            }
169            Ok(TableReference::Table {
170                name: name.to_string(),
171                alias: alias.as_ref().map(|a| a.name.value.clone()),
172            })
173        }
174        sql_ast::TableFactor::Derived {
175            subquery, alias, ..
176        } => {
177            let query = convert_query(subquery)?;
178            let alias_name = alias
179                .as_ref()
180                .map(|a| a.name.value.clone())
181                .ok_or_else(|| QueryError::semantic("Subquery must have an alias"))?;
182            Ok(TableReference::Subquery {
183                query: Box::new(query),
184                alias: alias_name,
185            })
186        }
187        _ => Err(QueryError::unsupported("Unsupported table reference")),
188    }
189}
190
191fn convert_join_type(op: &sql_ast::JoinOperator) -> Result<JoinType> {
192    match op {
193        sql_ast::JoinOperator::Inner(_) => Ok(JoinType::Inner),
194        sql_ast::JoinOperator::LeftOuter(_) => Ok(JoinType::Left),
195        sql_ast::JoinOperator::RightOuter(_) => Ok(JoinType::Right),
196        sql_ast::JoinOperator::FullOuter(_) => Ok(JoinType::Full),
197        sql_ast::JoinOperator::CrossJoin(_) => Ok(JoinType::Cross),
198        _ => Err(QueryError::unsupported("Unsupported join type")),
199    }
200}
201
202fn convert_join_constraint(constraint: &sql_ast::JoinConstraint) -> Result<Option<Expr>> {
203    match constraint {
204        sql_ast::JoinConstraint::On(expr) => Ok(Some(convert_expr(expr)?)),
205        sql_ast::JoinConstraint::Using(_) => {
206            Err(QueryError::unsupported("USING clause not supported"))
207        }
208        sql_ast::JoinConstraint::Natural => {
209            Err(QueryError::unsupported("NATURAL join not supported"))
210        }
211        sql_ast::JoinConstraint::None => Ok(None),
212    }
213}
214
215fn convert_expr(expr: &sql_ast::Expr) -> Result<Expr> {
216    match expr {
217        sql_ast::Expr::Identifier(ident) => Ok(Expr::Column {
218            table: None,
219            name: ident.value.clone(),
220        }),
221        sql_ast::Expr::CompoundIdentifier(parts) => {
222            if parts.len() == 2 {
223                Ok(Expr::Column {
224                    table: Some(parts[0].value.clone()),
225                    name: parts[1].value.clone(),
226                })
227            } else {
228                Err(QueryError::semantic("Invalid column reference"))
229            }
230        }
231        sql_ast::Expr::Value(value_with_span) => {
232            Ok(Expr::Literal(convert_value(&value_with_span.value)?))
233        }
234        sql_ast::Expr::BinaryOp { left, op, right } => Ok(Expr::BinaryOp {
235            left: Box::new(convert_expr(left)?),
236            op: convert_binary_op(op)?,
237            right: Box::new(convert_expr(right)?),
238        }),
239        sql_ast::Expr::UnaryOp { op, expr } => Ok(Expr::UnaryOp {
240            op: convert_unary_op(op)?,
241            expr: Box::new(convert_expr(expr)?),
242        }),
243        sql_ast::Expr::Function(func) => {
244            let name = func.name.to_string();
245            let mut args = Vec::new();
246
247            // Handle FunctionArguments enum
248            match &func.args {
249                sql_ast::FunctionArguments::None => {
250                    // No arguments
251                }
252                sql_ast::FunctionArguments::Subquery(_) => {
253                    return Err(QueryError::unsupported(
254                        "Subquery in function arguments not supported",
255                    ));
256                }
257                sql_ast::FunctionArguments::List(arg_list) => {
258                    for arg in &arg_list.args {
259                        match arg {
260                            sql_ast::FunctionArg::Unnamed(sql_ast::FunctionArgExpr::Expr(e)) => {
261                                args.push(convert_expr(e)?);
262                            }
263                            sql_ast::FunctionArg::Unnamed(sql_ast::FunctionArgExpr::Wildcard) => {
264                                // Handle COUNT(*) and similar
265                                args.push(Expr::Wildcard);
266                            }
267                            sql_ast::FunctionArg::Named {
268                                name: _,
269                                arg: sql_ast::FunctionArgExpr::Expr(e),
270                                ..
271                            } => {
272                                args.push(convert_expr(e)?);
273                            }
274                            sql_ast::FunctionArg::Named {
275                                name: _,
276                                arg: sql_ast::FunctionArgExpr::Wildcard,
277                                ..
278                            } => {
279                                args.push(Expr::Wildcard);
280                            }
281                            _ => {
282                                return Err(QueryError::unsupported(
283                                    "Unsupported function argument",
284                                ));
285                            }
286                        }
287                    }
288                }
289            }
290            Ok(Expr::Function { name, args })
291        }
292        sql_ast::Expr::Case {
293            operand,
294            conditions,
295            else_result,
296            ..
297        } => {
298            let operand = operand
299                .as_ref()
300                .map(|e| convert_expr(e))
301                .transpose()?
302                .map(Box::new);
303            let mut when_then = Vec::new();
304            for case_when in conditions.iter() {
305                when_then.push((
306                    convert_expr(&case_when.condition)?,
307                    convert_expr(&case_when.result)?,
308                ));
309            }
310            let else_result = else_result
311                .as_ref()
312                .map(|e| convert_expr(e))
313                .transpose()?
314                .map(Box::new);
315            Ok(Expr::Case {
316                operand,
317                when_then,
318                else_result,
319            })
320        }
321        sql_ast::Expr::Cast {
322            expr, data_type, ..
323        } => Ok(Expr::Cast {
324            expr: Box::new(convert_expr(expr)?),
325            data_type: convert_data_type(data_type)?,
326        }),
327        sql_ast::Expr::IsNull(expr) => Ok(Expr::IsNull(Box::new(convert_expr(expr)?))),
328        sql_ast::Expr::IsNotNull(expr) => Ok(Expr::IsNotNull(Box::new(convert_expr(expr)?))),
329        sql_ast::Expr::InList {
330            expr,
331            list,
332            negated,
333        } => {
334            let expr = Box::new(convert_expr(expr)?);
335            let list = list.iter().map(convert_expr).collect::<Result<Vec<_>>>()?;
336            Ok(Expr::InList {
337                expr,
338                list,
339                negated: *negated,
340            })
341        }
342        sql_ast::Expr::Between {
343            expr,
344            low,
345            high,
346            negated,
347        } => Ok(Expr::Between {
348            expr: Box::new(convert_expr(expr)?),
349            low: Box::new(convert_expr(low)?),
350            high: Box::new(convert_expr(high)?),
351            negated: *negated,
352        }),
353        sql_ast::Expr::Like {
354            negated,
355            expr,
356            pattern,
357            ..
358        } => Ok(Expr::BinaryOp {
359            left: Box::new(convert_expr(expr)?),
360            op: if *negated {
361                BinaryOperator::NotLike
362            } else {
363                BinaryOperator::Like
364            },
365            right: Box::new(convert_expr(pattern)?),
366        }),
367        sql_ast::Expr::ILike {
368            negated,
369            expr,
370            pattern,
371            ..
372        } => {
373            // ILIKE (case-insensitive) is treated as regular LIKE for now
374            Ok(Expr::BinaryOp {
375                left: Box::new(convert_expr(expr)?),
376                op: if *negated {
377                    BinaryOperator::NotLike
378                } else {
379                    BinaryOperator::Like
380                },
381                right: Box::new(convert_expr(pattern)?),
382            })
383        }
384        sql_ast::Expr::Subquery(query) => Ok(Expr::Subquery(Box::new(convert_query(query)?))),
385        sql_ast::Expr::Nested(expr) => convert_expr(expr),
386        sql_ast::Expr::Wildcard(_) => Ok(Expr::Wildcard),
387        _ => Err(QueryError::unsupported(format!(
388            "Unsupported expression: {:?}",
389            expr
390        ))),
391    }
392}
393
394fn convert_value(value: &sql_ast::Value) -> Result<Literal> {
395    match value {
396        sql_ast::Value::Null => Ok(Literal::Null),
397        sql_ast::Value::Boolean(b) => Ok(Literal::Boolean(*b)),
398        sql_ast::Value::Number(n, _) => {
399            if let Ok(i) = n.parse::<i64>() {
400                Ok(Literal::Integer(i))
401            } else if let Ok(f) = n.parse::<f64>() {
402                Ok(Literal::Float(f))
403            } else {
404                Err(QueryError::parse_error("Invalid number", 0, 0))
405            }
406        }
407        sql_ast::Value::SingleQuotedString(s) | sql_ast::Value::DoubleQuotedString(s) => {
408            Ok(Literal::String(s.clone()))
409        }
410        _ => Err(QueryError::unsupported("Unsupported literal value")),
411    }
412}
413
414fn convert_binary_op(op: &sql_ast::BinaryOperator) -> Result<BinaryOperator> {
415    match op {
416        sql_ast::BinaryOperator::Plus => Ok(BinaryOperator::Plus),
417        sql_ast::BinaryOperator::Minus => Ok(BinaryOperator::Minus),
418        sql_ast::BinaryOperator::Multiply => Ok(BinaryOperator::Multiply),
419        sql_ast::BinaryOperator::Divide => Ok(BinaryOperator::Divide),
420        sql_ast::BinaryOperator::Modulo => Ok(BinaryOperator::Modulo),
421        sql_ast::BinaryOperator::Eq => Ok(BinaryOperator::Eq),
422        sql_ast::BinaryOperator::NotEq => Ok(BinaryOperator::NotEq),
423        sql_ast::BinaryOperator::Lt => Ok(BinaryOperator::Lt),
424        sql_ast::BinaryOperator::LtEq => Ok(BinaryOperator::LtEq),
425        sql_ast::BinaryOperator::Gt => Ok(BinaryOperator::Gt),
426        sql_ast::BinaryOperator::GtEq => Ok(BinaryOperator::GtEq),
427        sql_ast::BinaryOperator::And => Ok(BinaryOperator::And),
428        sql_ast::BinaryOperator::Or => Ok(BinaryOperator::Or),
429        sql_ast::BinaryOperator::StringConcat => Ok(BinaryOperator::Concat),
430        // Note: LIKE and NOT LIKE are handled as separate expression types in sqlparser 0.52+
431        _ => Err(QueryError::unsupported("Unsupported binary operator")),
432    }
433}
434
435fn convert_unary_op(op: &sql_ast::UnaryOperator) -> Result<UnaryOperator> {
436    match op {
437        sql_ast::UnaryOperator::Minus => Ok(UnaryOperator::Minus),
438        sql_ast::UnaryOperator::Not => Ok(UnaryOperator::Not),
439        _ => Err(QueryError::unsupported("Unsupported unary operator")),
440    }
441}
442
443fn convert_order_by_expr(order: &sql_ast::OrderByExpr) -> Result<OrderByExpr> {
444    Ok(OrderByExpr {
445        expr: convert_expr(&order.expr)?,
446        asc: order.options.asc.unwrap_or(true),
447        nulls_first: order.options.nulls_first.unwrap_or(false),
448    })
449}
450
451fn convert_limit(limit: &sql_ast::Expr) -> Result<usize> {
452    match limit {
453        sql_ast::Expr::Value(value_with_span) => match &value_with_span.value {
454            sql_ast::Value::Number(n, _) => n
455                .parse::<usize>()
456                .map_err(|_| QueryError::semantic("Invalid LIMIT value")),
457            _ => Err(QueryError::semantic("LIMIT must be a number")),
458        },
459        _ => Err(QueryError::semantic("LIMIT must be a number")),
460    }
461}
462
463fn convert_offset(offset: &sql_ast::Offset) -> Result<usize> {
464    match &offset.value {
465        sql_ast::Expr::Value(value_with_span) => match &value_with_span.value {
466            sql_ast::Value::Number(n, _) => n
467                .parse::<usize>()
468                .map_err(|_| QueryError::semantic("Invalid OFFSET value")),
469            _ => Err(QueryError::semantic("OFFSET must be a number")),
470        },
471        _ => Err(QueryError::semantic("OFFSET must be a number")),
472    }
473}
474
475fn convert_data_type(data_type: &sql_ast::DataType) -> Result<DataType> {
476    match data_type {
477        sql_ast::DataType::Boolean => Ok(DataType::Boolean),
478        sql_ast::DataType::TinyInt(_) => Ok(DataType::Int8),
479        sql_ast::DataType::SmallInt(_) => Ok(DataType::Int16),
480        sql_ast::DataType::Int(_) | sql_ast::DataType::Integer(_) => Ok(DataType::Int32),
481        sql_ast::DataType::BigInt(_) => Ok(DataType::Int64),
482        sql_ast::DataType::TinyIntUnsigned(_) => Ok(DataType::UInt8),
483        sql_ast::DataType::SmallIntUnsigned(_) => Ok(DataType::UInt16),
484        sql_ast::DataType::IntUnsigned(_)
485        | sql_ast::DataType::IntegerUnsigned(_)
486        | sql_ast::DataType::UnsignedInteger => Ok(DataType::UInt32),
487        sql_ast::DataType::BigIntUnsigned(_) => Ok(DataType::UInt64),
488        sql_ast::DataType::Float(_) | sql_ast::DataType::Real => Ok(DataType::Float32),
489        sql_ast::DataType::Double(_) | sql_ast::DataType::DoublePrecision => Ok(DataType::Float64),
490        sql_ast::DataType::Varchar(_)
491        | sql_ast::DataType::Char(_)
492        | sql_ast::DataType::Text
493        | sql_ast::DataType::String(_) => Ok(DataType::String),
494        sql_ast::DataType::Binary(_) | sql_ast::DataType::Varbinary(_) => Ok(DataType::Binary),
495        sql_ast::DataType::Timestamp(_, _) => Ok(DataType::Timestamp),
496        sql_ast::DataType::Date => Ok(DataType::Date),
497        sql_ast::DataType::Custom(name, _) if name.to_string().to_uppercase() == "GEOMETRY" => {
498            Ok(DataType::Geometry)
499        }
500        _ => Err(QueryError::unsupported(format!(
501            "Unsupported data type: {:?}",
502            data_type
503        ))),
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510
511    #[test]
512    fn test_parse_simple_select() {
513        let sql = "SELECT id, name FROM users";
514        let result = parse_sql(sql);
515        assert!(result.is_ok());
516    }
517
518    #[test]
519    fn test_parse_select_with_where() {
520        let sql = "SELECT * FROM users WHERE age > 18";
521        let result = parse_sql(sql);
522        assert!(result.is_ok());
523    }
524
525    #[test]
526    fn test_parse_select_with_join() {
527        let sql = "SELECT u.name, o.total FROM users u INNER JOIN orders o ON u.id = o.user_id";
528        let result = parse_sql(sql);
529        assert!(result.is_ok());
530    }
531
532    #[test]
533    fn test_parse_spatial_function() {
534        let sql = "SELECT ST_Area(geom) FROM buildings";
535        let result = parse_sql(sql);
536        assert!(result.is_ok());
537    }
538}