Skip to main content

nodedb_sql/resolver/expr/convert/
literals.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Literal, cast, array, and interval conversion.
4
5use sqlparser::ast::{Array, DataType, Expr, Interval, TypedString, Value, ValueWithSpan};
6
7use crate::error::{Result, SqlError};
8use crate::resolver::ColumnScope;
9use crate::resolver::expr::value::{convert_value, parse_interval_to_micros};
10use crate::types::*;
11
12use super::entry::convert_expr_depth;
13
14pub(super) fn convert_value_expr(val: &ValueWithSpan) -> Result<SqlExpr> {
15    Ok(SqlExpr::Literal(convert_value(&val.value)?))
16}
17
18pub(super) fn convert_typed_string(ts: &TypedString) -> Result<SqlExpr> {
19    // TIMESTAMP '...' and TIMESTAMPTZ '...' typed string literals.
20    let type_str = format!("{}", ts.data_type).to_ascii_uppercase();
21    let raw = match &ts.value.value {
22        Value::SingleQuotedString(s) => s.clone(),
23        other => {
24            return Err(SqlError::Unsupported {
25                detail: format!("typed string value: {other}"),
26            });
27        }
28    };
29    match type_str.as_str() {
30        "TIMESTAMP" => {
31            let dt = nodedb_types::NdbDateTime::parse(&raw).ok_or_else(|| SqlError::Parse {
32                detail: format!("cannot parse TIMESTAMP literal: '{raw}'"),
33            })?;
34            return Ok(SqlExpr::Literal(SqlValue::Timestamp(dt)));
35        }
36        "TIMESTAMPTZ" | "TIMESTAMP WITH TIME ZONE" => {
37            let dt = nodedb_types::NdbDateTime::parse(&raw).ok_or_else(|| SqlError::Parse {
38                detail: format!("cannot parse TIMESTAMPTZ literal: '{raw}'"),
39            })?;
40            return Ok(SqlExpr::Literal(SqlValue::Timestamptz(dt)));
41        }
42        _ => {}
43    }
44    // Fall through: return as a generic literal string.
45    Ok(SqlExpr::Literal(SqlValue::String(raw)))
46}
47
48pub(super) fn convert_cast(
49    expr: &Expr,
50    data_type: &DataType,
51    depth: &mut usize,
52    scope: &ColumnScope<'_>,
53) -> Result<SqlExpr> {
54    // `::tsvector` and `::tsquery` casts are PG surface notation; the
55    // inner expression is the actual text value.  Elide the cast and
56    // return the inner expression directly — no runtime type change is
57    // needed since we operate on plain strings internally.
58    let type_str = format!("{data_type}").to_ascii_lowercase();
59    if type_str == "tsvector" || type_str == "tsquery" {
60        return convert_expr_depth(expr, depth, scope);
61    }
62    // `'...'::TIMESTAMP` and `'...'::TIMESTAMPTZ` — promote string literals
63    // to typed SqlValue when the inner expression is a string literal.
64    let upper = type_str.to_uppercase();
65    if (upper == "TIMESTAMP" || upper == "TIMESTAMPTZ" || upper == "TIMESTAMP WITH TIME ZONE")
66        && let Expr::Value(v) = expr
67        && let Value::SingleQuotedString(s) = &v.value
68    {
69        let dt = nodedb_types::NdbDateTime::parse(s).ok_or_else(|| SqlError::Parse {
70            detail: format!("cannot parse timestamp cast: '{s}'"),
71        })?;
72        return Ok(SqlExpr::Literal(if upper == "TIMESTAMP" {
73            SqlValue::Timestamp(dt)
74        } else {
75            SqlValue::Timestamptz(dt)
76        }));
77    }
78    Ok(SqlExpr::Cast {
79        expr: Box::new(convert_expr_depth(expr, depth, scope)?),
80        to_type: format!("{data_type}"),
81    })
82}
83
84pub(super) fn convert_array(
85    array: &Array,
86    depth: &mut usize,
87    scope: &ColumnScope<'_>,
88) -> Result<SqlExpr> {
89    let elems = array
90        .elem
91        .iter()
92        .map(|e| convert_expr_depth(e, depth, scope))
93        .collect::<Result<_>>()?;
94    Ok(SqlExpr::ArrayLiteral(elems))
95}
96
97pub(super) fn convert_wildcard() -> Result<SqlExpr> {
98    Ok(SqlExpr::Wildcard)
99}
100
101pub(super) fn convert_interval(interval: &Interval) -> Result<SqlExpr> {
102    // INTERVAL '1 hour' → microseconds as i64 literal.
103    // The interval value is typically a string literal.
104    let interval_str = match interval.value.as_ref() {
105        Expr::Value(v) => match &v.value {
106            Value::SingleQuotedString(s) => s.clone(),
107            Value::Number(n, _) => {
108                // INTERVAL 5 HOUR → combine number with leading_field.
109                if let Some(ref field) = interval.leading_field {
110                    format!("{n} {field}")
111                } else {
112                    n.clone()
113                }
114            }
115            _ => {
116                return Err(SqlError::Unsupported {
117                    detail: format!("INTERVAL value: {}", interval.value),
118                });
119            }
120        },
121        _ => {
122            return Err(SqlError::Unsupported {
123                detail: format!("INTERVAL expression: {}", interval.value),
124            });
125        }
126    };
127
128    // If leading_field is specified, append it: INTERVAL '5' HOUR → "5 HOUR"
129    let full_str = if interval_str.chars().all(|c| c.is_ascii_digit())
130        && let Some(ref field) = interval.leading_field
131    {
132        format!("{interval_str} {field}")
133    } else {
134        interval_str
135    };
136
137    let micros = parse_interval_to_micros(&full_str).ok_or_else(|| SqlError::Parse {
138        detail: format!("cannot parse INTERVAL '{full_str}'"),
139    })?;
140
141    Ok(SqlExpr::Literal(SqlValue::Int(micros)))
142}
143
144#[cfg(test)]
145mod tests {
146    use sqlparser::ast::{Expr, Value};
147
148    use crate::error::SqlError;
149    use crate::resolver::expr::convert::entry::tests::{first_select_expr, select_expr_lowered};
150    use crate::resolver::expr::value::convert_value;
151    use crate::types::*;
152
153    /// `'userId'` is a single-quoted string literal.
154    #[test]
155    fn single_quoted_is_string_literal() {
156        let expr = first_select_expr("SELECT 'userId' FROM users");
157        match &expr {
158            Expr::Value(v) => match &v.value {
159                Value::SingleQuotedString(s) => assert_eq!(s, "userId"),
160                other => panic!("expected SingleQuotedString, got {other:?}"),
161            },
162            other => panic!("expected Value, got {other:?}"),
163        }
164        // And convert_value maps it to SqlValue::String.
165        let Expr::Value(v) = expr else { unreachable!() };
166        assert!(matches!(
167            convert_value(&v.value),
168            Ok(SqlValue::String(s)) if s == "userId"
169        ));
170    }
171
172    /// `Value::DoubleQuotedString` (non-Postgres dialect) falls through
173    /// `convert_value` to `SqlError::Unsupported`. With PostgreSQL dialect
174    /// this variant is never produced, but constructing it directly verifies
175    /// the arm is absent and not silently accepted.
176    #[test]
177    fn double_quoted_string_value_unsupported() {
178        // Construct the variant directly — it cannot be produced by parsing
179        // with PostgreSqlDialect, which is exactly why the arm is dead code.
180        let val = Value::DoubleQuotedString("userId".into());
181        assert!(
182            matches!(convert_value(&val), Err(SqlError::Unsupported { .. })),
183            "DoubleQuotedString should be Unsupported, not silently accepted"
184        );
185    }
186
187    #[test]
188    fn tsvector_cast_elided() {
189        // 'foo'::tsvector → Literal("foo")
190        let expr = select_expr_lowered("SELECT 'foo'::tsvector FROM t");
191        assert!(
192            matches!(expr, SqlExpr::Literal(SqlValue::String(ref s)) if s == "foo"),
193            "expected Literal(\"foo\"), got {expr:?}"
194        );
195    }
196
197    #[test]
198    fn tsquery_cast_elided() {
199        // 'rust'::tsquery → Literal("rust")
200        let expr = select_expr_lowered("SELECT 'rust'::tsquery FROM t");
201        assert!(
202            matches!(expr, SqlExpr::Literal(SqlValue::String(ref s)) if s == "rust"),
203            "expected Literal(\"rust\"), got {expr:?}"
204        );
205    }
206}