llkv_plan/
conversion.rs

1//! Conversion utilities from SQL AST to Plan types.
2//!
3//! This module provides functions for converting sqlparser AST nodes into
4//! llkv-plan data structures, particularly for literal value conversion
5//! and range SELECT parsing.
6
7use llkv_result::{Error, Result};
8use sqlparser::ast::{
9    Expr as SqlExpr, FunctionArg, FunctionArgExpr, GroupByExpr, ObjectName, ObjectNamePart, Select,
10    SelectItem, SelectItemQualifiedWildcardKind, TableAlias, TableFactor, UnaryOperator, Value,
11    ValueWithSpan,
12};
13
14use crate::PlanValue;
15
16/// Convert a SQL expression to a PlanValue literal.
17///
18/// Supports:
19/// - Literal values (numbers, strings, NULL)
20/// - Unary operators (-, +)
21/// - Nested expressions
22/// - Dictionary/struct literals
23///
24/// # Examples
25///
26/// ```ignore
27/// use llkv_plan::conversion::plan_value_from_sql_expr;
28/// use sqlparser::ast::Expr;
29///
30/// let expr = /* parse SQL expression */;
31/// let value = plan_value_from_sql_expr(&expr)?;
32/// ```
33///
34/// # Errors
35///
36/// Returns an error for:
37/// - Non-literal expressions (column references, function calls, etc.)
38/// - Unsupported operators
39/// - Invalid number parsing
40pub fn plan_value_from_sql_expr(expr: &SqlExpr) -> Result<PlanValue> {
41    match expr {
42        SqlExpr::Value(value) => plan_value_from_sql_value(value),
43        SqlExpr::UnaryOp {
44            op: UnaryOperator::Minus,
45            expr,
46        } => match plan_value_from_sql_expr(expr)? {
47            PlanValue::Integer(v) => Ok(PlanValue::Integer(-v)),
48            PlanValue::Float(v) => Ok(PlanValue::Float(-v)),
49            PlanValue::Null | PlanValue::String(_) | PlanValue::Struct(_) => Err(
50                Error::InvalidArgumentError("cannot negate non-numeric literal".into()),
51            ),
52        },
53        SqlExpr::UnaryOp {
54            op: UnaryOperator::Plus,
55            expr,
56        } => plan_value_from_sql_expr(expr),
57        SqlExpr::Nested(inner) => plan_value_from_sql_expr(inner),
58        SqlExpr::Dictionary(fields) => {
59            let mut map = std::collections::HashMap::new();
60            for field in fields {
61                let key = field.key.value.clone();
62                let value = plan_value_from_sql_expr(&field.value)?;
63                map.insert(key, value);
64            }
65            Ok(PlanValue::Struct(map))
66        }
67        other => Err(Error::InvalidArgumentError(format!(
68            "unsupported literal expression: {other:?}"
69        ))),
70    }
71}
72
73/// Convert a SQL value literal to a PlanValue.
74///
75/// Handles:
76/// - NULL
77/// - Numbers (integers and floats)
78/// - Strings
79///
80/// # Examples
81///
82/// ```ignore
83/// use llkv_plan::conversion::plan_value_from_sql_value;
84/// use sqlparser::ast::ValueWithSpan;
85///
86/// let value = /* parse SQL value */;
87/// let plan_value = plan_value_from_sql_value(&value)?;
88/// ```
89///
90/// # Errors
91///
92/// Returns an error for:
93/// - Boolean literals (not yet supported)
94/// - Invalid number formats
95/// - Unsupported value types
96pub fn plan_value_from_sql_value(value: &ValueWithSpan) -> Result<PlanValue> {
97    match &value.value {
98        Value::Null => Ok(PlanValue::Null),
99        Value::Number(text, _) => {
100            if text.contains(['.', 'e', 'E']) {
101                let parsed = text.parse::<f64>().map_err(|err| {
102                    Error::InvalidArgumentError(format!("invalid float literal: {err}"))
103                })?;
104                Ok(PlanValue::Float(parsed))
105            } else {
106                let parsed = text.parse::<i64>().map_err(|err| {
107                    Error::InvalidArgumentError(format!("invalid integer literal: {err}"))
108                })?;
109                Ok(PlanValue::Integer(parsed))
110            }
111        }
112        Value::Boolean(_) => Err(Error::InvalidArgumentError(
113            "BOOLEAN literals are not supported yet".into(),
114        )),
115        other => {
116            if let Some(text) = other.clone().into_string() {
117                Ok(PlanValue::String(text))
118            } else {
119                Err(Error::InvalidArgumentError(format!(
120                    "unsupported literal: {other:?}"
121                )))
122            }
123        }
124    }
125}
126
127// ============================================================================
128// Range SELECT Support
129// ============================================================================
130
131/// Result of parsing a range() SELECT statement.
132#[derive(Clone)]
133pub struct RangeSelectRows {
134    rows: Vec<Vec<PlanValue>>,
135}
136
137impl RangeSelectRows {
138    /// Convert into the underlying rows vector.
139    pub fn into_rows(self) -> Vec<Vec<PlanValue>> {
140        self.rows
141    }
142}
143
144#[derive(Clone)]
145enum RangeProjection {
146    Column,
147    Literal(PlanValue),
148}
149
150#[derive(Clone)]
151struct RangeSpec {
152    start: i64,
153    #[allow(dead_code)] // Used for validation, computed into row_count
154    end: i64,
155    row_count: usize,
156    column_name_lower: String,
157    table_alias_lower: Option<String>,
158}
159
160impl RangeSpec {
161    fn matches_identifier(&self, ident: &str) -> bool {
162        let lower = ident.to_ascii_lowercase();
163        lower == self.column_name_lower || lower == "range"
164    }
165
166    fn matches_table_alias(&self, ident: &str) -> bool {
167        let lower = ident.to_ascii_lowercase();
168        match &self.table_alias_lower {
169            Some(alias) => lower == *alias,
170            None => lower == "range",
171        }
172    }
173
174    fn matches_object_name(&self, name: &ObjectName) -> bool {
175        if name.0.len() != 1 {
176            return false;
177        }
178        match &name.0[0] {
179            ObjectNamePart::Identifier(ident) => self.matches_table_alias(&ident.value),
180            _ => false,
181        }
182    }
183}
184
185/// Extract rows from a range() SELECT statement.
186///
187/// Parses SELECT statements of the form:
188/// - `SELECT * FROM range(10)`
189/// - `SELECT * FROM range(5, 15)`  
190/// - `SELECT range, range * 2 FROM range(100)`
191///
192/// Returns `None` if the SELECT is not a range() query.
193///
194/// # Errors
195///
196/// Returns an error if:
197/// - The range() function has invalid arguments
198/// - Unsupported SELECT clauses are used (WHERE, JOIN, etc.)
199/// - Column references don't match the range source
200pub fn extract_rows_from_range(select: &Select) -> Result<Option<RangeSelectRows>> {
201    let spec = match parse_range_spec(select)? {
202        Some(spec) => spec,
203        None => return Ok(None),
204    };
205
206    if select.selection.is_some() {
207        return Err(Error::InvalidArgumentError(
208            "WHERE clauses are not supported for range() SELECT statements".into(),
209        ));
210    }
211    if select.having.is_some()
212        || !select.named_window.is_empty()
213        || select.qualify.is_some()
214        || select.distinct.is_some()
215        || select.top.is_some()
216        || select.into.is_some()
217        || select.prewhere.is_some()
218        || !select.lateral_views.is_empty()
219        || select.value_table_mode.is_some()
220        || !group_by_is_empty(&select.group_by)
221    {
222        return Err(Error::InvalidArgumentError(
223            "advanced SELECT clauses are not supported for range() SELECT statements".into(),
224        ));
225    }
226
227    let mut projections: Vec<RangeProjection> = Vec::with_capacity(select.projection.len());
228
229    // If projection is empty, treat it as SELECT * (implicit wildcard)
230    if select.projection.is_empty() {
231        projections.push(RangeProjection::Column);
232    } else {
233        for item in &select.projection {
234            let projection = match item {
235                SelectItem::Wildcard(_) => RangeProjection::Column,
236                SelectItem::QualifiedWildcard(kind, _) => match kind {
237                    SelectItemQualifiedWildcardKind::ObjectName(object_name) => {
238                        if spec.matches_object_name(object_name) {
239                            RangeProjection::Column
240                        } else {
241                            return Err(Error::InvalidArgumentError(
242                                "qualified wildcard must reference the range() source".into(),
243                            ));
244                        }
245                    }
246                    SelectItemQualifiedWildcardKind::Expr(_) => {
247                        return Err(Error::InvalidArgumentError(
248                            "expression-qualified wildcards are not supported for range() SELECT statements".into(),
249                        ));
250                    }
251                },
252                SelectItem::UnnamedExpr(expr) => build_range_projection_expr(expr, &spec)?,
253                SelectItem::ExprWithAlias { expr, .. } => build_range_projection_expr(expr, &spec)?,
254            };
255            projections.push(projection);
256        }
257    }
258
259    let mut rows: Vec<Vec<PlanValue>> = Vec::with_capacity(spec.row_count);
260    for idx in 0..spec.row_count {
261        let mut row: Vec<PlanValue> = Vec::with_capacity(projections.len());
262        let value = spec.start + (idx as i64);
263        for projection in &projections {
264            match projection {
265                RangeProjection::Column => row.push(PlanValue::Integer(value)),
266                RangeProjection::Literal(value) => row.push(value.clone()),
267            }
268        }
269        rows.push(row);
270    }
271
272    Ok(Some(RangeSelectRows { rows }))
273}
274
275fn build_range_projection_expr(expr: &SqlExpr, spec: &RangeSpec) -> Result<RangeProjection> {
276    match expr {
277        SqlExpr::Identifier(ident) => {
278            if spec.matches_identifier(&ident.value) {
279                Ok(RangeProjection::Column)
280            } else {
281                Err(Error::InvalidArgumentError(format!(
282                    "unknown column '{}' in range() SELECT",
283                    ident.value
284                )))
285            }
286        }
287        SqlExpr::CompoundIdentifier(parts) => {
288            if parts.len() == 2
289                && spec.matches_table_alias(&parts[0].value)
290                && spec.matches_identifier(&parts[1].value)
291            {
292                Ok(RangeProjection::Column)
293            } else {
294                Err(Error::InvalidArgumentError(
295                    "compound identifiers must reference the range() source".into(),
296                ))
297            }
298        }
299        SqlExpr::Wildcard(_) | SqlExpr::QualifiedWildcard(_, _) => unreachable!(),
300        other => Ok(RangeProjection::Literal(plan_value_from_sql_expr(other)?)),
301    }
302}
303
304fn parse_range_spec(select: &Select) -> Result<Option<RangeSpec>> {
305    if select.from.len() != 1 {
306        return Ok(None);
307    }
308    let item = &select.from[0];
309    if !item.joins.is_empty() {
310        return Err(Error::InvalidArgumentError(
311            "JOIN clauses are not supported for range() SELECT statements".into(),
312        ));
313    }
314
315    match &item.relation {
316        TableFactor::Function {
317            lateral,
318            name,
319            args,
320            alias,
321        } => {
322            if *lateral {
323                return Err(Error::InvalidArgumentError(
324                    "LATERAL range() is not supported".into(),
325                ));
326            }
327            parse_range_spec_from_args(name, args, alias)
328        }
329        TableFactor::Table {
330            name,
331            alias,
332            args: Some(table_args),
333            with_ordinality,
334            ..
335        } => {
336            if *with_ordinality {
337                return Err(Error::InvalidArgumentError(
338                    "WITH ORDINALITY is not supported for range()".into(),
339                ));
340            }
341            if table_args.settings.is_some() {
342                return Err(Error::InvalidArgumentError(
343                    "range() SETTINGS clause is not supported".into(),
344                ));
345            }
346            parse_range_spec_from_args(name, &table_args.args, alias)
347        }
348        _ => Ok(None),
349    }
350}
351
352fn parse_range_spec_from_args(
353    name: &ObjectName,
354    args: &[FunctionArg],
355    alias: &Option<TableAlias>,
356) -> Result<Option<RangeSpec>> {
357    if name.0.len() != 1 {
358        return Ok(None);
359    }
360    let func_name = match &name.0[0] {
361        ObjectNamePart::Identifier(ident) => ident.value.to_ascii_lowercase(),
362        _ => return Ok(None),
363    };
364    if func_name != "range" {
365        return Ok(None);
366    }
367
368    if args.is_empty() || args.len() > 2 {
369        return Err(Error::InvalidArgumentError(
370            "range() requires one or two arguments".into(),
371        ));
372    }
373
374    // Helper to extract integer from argument
375    let extract_int = |arg: &FunctionArg| -> Result<i64> {
376        let arg_expr = match arg {
377            FunctionArg::Unnamed(FunctionArgExpr::Expr(expr)) => expr,
378            FunctionArg::Unnamed(FunctionArgExpr::QualifiedWildcard(_))
379            | FunctionArg::Unnamed(FunctionArgExpr::Wildcard) => {
380                return Err(Error::InvalidArgumentError(
381                    "range() argument must be an integer literal".into(),
382                ));
383            }
384            FunctionArg::Named { .. } | FunctionArg::ExprNamed { .. } => {
385                return Err(Error::InvalidArgumentError(
386                    "named arguments are not supported for range()".into(),
387                ));
388            }
389        };
390
391        let value = plan_value_from_sql_expr(arg_expr)?;
392        match value {
393            PlanValue::Integer(v) => Ok(v),
394            _ => Err(Error::InvalidArgumentError(
395                "range() argument must be an integer literal".into(),
396            )),
397        }
398    };
399
400    let (start, end, row_count) = if args.len() == 1 {
401        // range(count) - generate [0, count)
402        let count = extract_int(&args[0])?;
403        if count < 0 {
404            return Err(Error::InvalidArgumentError(
405                "range() argument must be non-negative".into(),
406            ));
407        }
408        (0, count, count as usize)
409    } else {
410        // range(start, end) - generate [start, end)
411        let start = extract_int(&args[0])?;
412        let end = extract_int(&args[1])?;
413        if end < start {
414            return Err(Error::InvalidArgumentError(
415                "range() end must be >= start".into(),
416            ));
417        }
418        let row_count = (end - start) as usize;
419        (start, end, row_count)
420    };
421
422    let column_name_lower = alias
423        .as_ref()
424        .and_then(|a| {
425            a.columns
426                .first()
427                .map(|col| col.name.value.to_ascii_lowercase())
428        })
429        .unwrap_or_else(|| "range".to_string());
430    let table_alias_lower = alias.as_ref().map(|a| a.name.value.to_ascii_lowercase());
431
432    Ok(Some(RangeSpec {
433        start,
434        end,
435        row_count,
436        column_name_lower,
437        table_alias_lower,
438    }))
439}
440
441fn group_by_is_empty(expr: &GroupByExpr) -> bool {
442    matches!(
443        expr,
444        GroupByExpr::Expressions(exprs, modifiers)
445            if exprs.is_empty() && modifiers.is_empty()
446    )
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452    use sqlparser::ast::{Expr as SqlExpr, Value, ValueWithSpan};
453
454    fn value_with_span(v: Value) -> ValueWithSpan {
455        ValueWithSpan {
456            value: v,
457            span: sqlparser::tokenizer::Span::empty(),
458        }
459    }
460
461    #[test]
462    fn test_null_value() {
463        let value = value_with_span(Value::Null);
464        assert_eq!(plan_value_from_sql_value(&value).unwrap(), PlanValue::Null);
465    }
466
467    #[test]
468    fn test_integer_value() {
469        let value = value_with_span(Value::Number("42".to_string(), false));
470        assert_eq!(
471            plan_value_from_sql_value(&value).unwrap(),
472            PlanValue::Integer(42)
473        );
474    }
475
476    #[test]
477    fn test_negative_integer() {
478        let value = value_with_span(Value::Number("42".to_string(), false));
479        let expr = SqlExpr::UnaryOp {
480            op: UnaryOperator::Minus,
481            expr: Box::new(SqlExpr::Value(value)),
482        };
483        assert_eq!(
484            plan_value_from_sql_expr(&expr).unwrap(),
485            PlanValue::Integer(-42)
486        );
487    }
488
489    #[test]
490    #[allow(clippy::approx_constant)]
491    fn test_float_value() {
492        let value = value_with_span(Value::Number("3.14".to_string(), false));
493        assert_eq!(
494            plan_value_from_sql_value(&value).unwrap(),
495            PlanValue::Float(3.14)
496        );
497    }
498
499    #[test]
500    fn test_string_value() {
501        let value = value_with_span(Value::SingleQuotedString("hello".to_string()));
502        assert_eq!(
503            plan_value_from_sql_value(&value).unwrap(),
504            PlanValue::String("hello".to_string())
505        );
506    }
507
508    #[test]
509    fn test_nested_expression() {
510        let value = value_with_span(Value::Number("100".to_string(), false));
511        let expr = SqlExpr::Nested(Box::new(SqlExpr::Value(value)));
512        assert_eq!(
513            plan_value_from_sql_expr(&expr).unwrap(),
514            PlanValue::Integer(100)
515        );
516    }
517
518    #[test]
519    fn test_plus_operator() {
520        let value = value_with_span(Value::Number("50".to_string(), false));
521        let expr = SqlExpr::UnaryOp {
522            op: UnaryOperator::Plus,
523            expr: Box::new(SqlExpr::Value(value)),
524        };
525        assert_eq!(
526            plan_value_from_sql_expr(&expr).unwrap(),
527            PlanValue::Integer(50)
528        );
529    }
530
531    #[test]
532    fn test_cannot_negate_string() {
533        let value = value_with_span(Value::SingleQuotedString("test".to_string()));
534        let expr = SqlExpr::UnaryOp {
535            op: UnaryOperator::Minus,
536            expr: Box::new(SqlExpr::Value(value)),
537        };
538        assert!(plan_value_from_sql_expr(&expr).is_err());
539    }
540
541    #[test]
542    fn test_boolean_not_supported() {
543        let value = value_with_span(Value::Boolean(true));
544        assert!(plan_value_from_sql_value(&value).is_err());
545    }
546}