Skip to main content

nodedb_sql/resolver/expr/
value.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use sqlparser::ast::Value;
4
5use crate::error::{Result, SqlError};
6use crate::types::*;
7
8/// Convert a sqlparser `Value` to our `SqlValue`.
9///
10/// Number literal routing:
11/// - Integers in `i64` range → `SqlValue::Int`.
12/// - Larger integers → `SqlValue::Decimal`, exact. A `u64` above `i64::MAX`
13///   is the `Decimal` `Value::from_u64` gives it, so it is written as a
14///   msgpack `uint64`. An integer past the `Decimal` range is
15///   [`SqlError::NumericLiteralOutOfRange`]: rounding it to a float would
16///   store another number than the one written.
17/// - Numbers with an exponent (`e` or `E`) → `SqlValue::Float`. A bound
18///   float parameter renders in this form, so it stays a float.
19/// - Other numbers with `.` → `SqlValue::Decimal` (exact arithmetic).
20/// - If decimal parse fails → fallback to `SqlValue::Float`, then `SqlValue::String`.
21pub fn convert_value(val: &Value) -> Result<SqlValue> {
22    match val {
23        Value::Number(n, _) => {
24            if let Ok(i) = n.parse::<i64>() {
25                Ok(SqlValue::Int(i))
26            } else if n.contains('e') || n.contains('E') {
27                match n.parse::<f64>() {
28                    Ok(f) => Ok(SqlValue::Float(f)),
29                    Err(_) => Ok(SqlValue::String(n.clone())),
30                }
31            } else if n.contains('.') {
32                // A fractional literal: prefer exact Decimal.
33                if let Ok(d) = rust_decimal::Decimal::from_str_exact(n) {
34                    Ok(SqlValue::Decimal(d))
35                } else if let Ok(f) = n.parse::<f64>() {
36                    Ok(SqlValue::Float(f))
37                } else {
38                    Ok(SqlValue::String(n.clone()))
39                }
40            } else if n.bytes().all(|b| b.is_ascii_digit()) {
41                integer_literal(n)
42            } else if let Ok(f) = n.parse::<f64>() {
43                Ok(SqlValue::Float(f))
44            } else {
45                Ok(SqlValue::String(n.clone()))
46            }
47        }
48        Value::SingleQuotedString(s) => Ok(SqlValue::String(s.clone())),
49        Value::Boolean(b) => Ok(SqlValue::Bool(*b)),
50        Value::Null => Ok(SqlValue::Null),
51        // `X'...'` hex-string literal → raw bytes. Enables byte-typed
52        // arguments such as WKB geometry (`ST_GeomFromWKB(X'01...')`).
53        Value::HexStringLiteral(s) => Ok(SqlValue::Bytes(decode_hex_literal(s)?)),
54        _ => Err(SqlError::Unsupported {
55            detail: format!("value literal: {val}"),
56        }),
57    }
58}
59
60/// An all-digit literal past `i64::MAX` as an exact `Decimal`.
61fn integer_literal(digits: &str) -> Result<SqlValue> {
62    if let Ok(u) = digits.parse::<u64>() {
63        return Ok(SqlValue::Decimal(rust_decimal::Decimal::from(u)));
64    }
65    rust_decimal::Decimal::from_str_exact(digits)
66        .map(SqlValue::Decimal)
67        .map_err(|_| SqlError::NumericLiteralOutOfRange {
68            literal: digits.to_string(),
69        })
70}
71
72/// Decode an even-length ASCII hex string (the inner text of an `X'...'`
73/// literal) into its byte sequence.
74fn decode_hex_literal(s: &str) -> Result<Vec<u8>> {
75    parse_hex_bytes(s).map_err(|reason| SqlError::Parse {
76        detail: format!("hex literal {reason}: X'{s}'"),
77    })
78}
79
80/// Parse an even-length ASCII hex string into bytes. Shared by `X'...'`
81/// literal decoding here and plain hex-string WKB arguments in
82/// `planner::spatial_ctor`, so the two surfaces never drift. Returns the
83/// failure reason as a fragment; callers wrap it into their own contextual
84/// `SqlError` variant.
85pub(crate) fn parse_hex_bytes(s: &str) -> std::result::Result<Vec<u8>, &'static str> {
86    if !s.len().is_multiple_of(2) {
87        return Err("must have an even number of digits");
88    }
89    (0..s.len())
90        .step_by(2)
91        .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|_| "contains an invalid hex digit"))
92        .collect()
93}
94
95/// Parse an interval string to microseconds.
96///
97/// Delegates to `nodedb_types::kv_parsing::parse_interval_to_ms` (ms → μs)
98/// and `NdbDuration::parse` for compound shorthand forms.
99pub(super) fn parse_interval_to_micros(s: &str) -> Option<i64> {
100    let s = s.trim();
101    if s.is_empty() {
102        return None;
103    }
104
105    // Try NdbDuration::parse first (handles compound "1h30m", "500ms", "2d").
106    if let Some(dur) = nodedb_types::NdbDuration::parse(s) {
107        return Some(dur.micros);
108    }
109
110    // Delegate to shared interval parser (handles all forms including compound).
111    if let Ok(ms) = nodedb_types::kv_parsing::parse_interval_to_ms(s) {
112        return Some(ms as i64 * 1000); // ms → μs
113    }
114
115    None
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    fn number(n: &str) -> Value {
123        Value::Number(n.to_string(), false)
124    }
125
126    #[test]
127    fn integer_literal_past_i64_stays_exact() {
128        assert_eq!(
129            convert_value(&number("9223372036854775807")).unwrap(),
130            SqlValue::Int(i64::MAX)
131        );
132        assert_eq!(
133            convert_value(&number("18446744073709551615")).unwrap(),
134            SqlValue::Decimal(rust_decimal::Decimal::from(u64::MAX))
135        );
136        // Past `u64`, inside the 96-bit `Decimal` mantissa.
137        assert_eq!(
138            convert_value(&number("79228162514264337593543950335")).unwrap(),
139            SqlValue::Decimal(rust_decimal::Decimal::MAX)
140        );
141    }
142
143    #[test]
144    fn an_exponent_literal_is_a_float_and_a_fraction_is_a_decimal() {
145        assert_eq!(
146            convert_value(&number("1.5e0")).unwrap(),
147            SqlValue::Float(1.5)
148        );
149        assert_eq!(convert_value(&number("2E0")).unwrap(), SqlValue::Float(2.0));
150        assert_eq!(
151            convert_value(&number("1e300")).unwrap(),
152            SqlValue::Float(1e300)
153        );
154        assert_eq!(
155            convert_value(&number("1.5")).unwrap(),
156            SqlValue::Decimal(rust_decimal::Decimal::new(15, 1))
157        );
158    }
159
160    #[test]
161    fn integer_literal_past_decimal_range_is_an_error() {
162        let err = convert_value(&number("79228162514264337593543950336")).unwrap_err();
163        assert!(
164            matches!(err, SqlError::NumericLiteralOutOfRange { ref literal }
165                if literal == "79228162514264337593543950336"),
166            "{err:?}"
167        );
168    }
169
170    #[test]
171    fn parse_interval_sql_word_forms() {
172        assert_eq!(parse_interval_to_micros("1 hour"), Some(3_600_000_000));
173        assert_eq!(parse_interval_to_micros("5 days"), Some(5 * 86_400_000_000));
174        assert_eq!(
175            parse_interval_to_micros("30 minutes"),
176            Some(30 * 60_000_000)
177        );
178        assert_eq!(
179            parse_interval_to_micros("2 hours 30 minutes"),
180            Some(9_000_000_000)
181        );
182        assert_eq!(parse_interval_to_micros("1 week"), Some(604_800_000_000));
183        assert_eq!(parse_interval_to_micros("100 milliseconds"), Some(100_000));
184    }
185
186    #[test]
187    fn parse_interval_shorthand() {
188        assert_eq!(parse_interval_to_micros("1h"), Some(3_600_000_000));
189        assert_eq!(parse_interval_to_micros("30m"), Some(30 * 60_000_000));
190        assert_eq!(parse_interval_to_micros("1h30m"), Some(5_400_000_000));
191        assert_eq!(parse_interval_to_micros("500ms"), Some(500_000));
192    }
193
194    #[test]
195    fn parse_interval_invalid() {
196        assert_eq!(parse_interval_to_micros(""), None);
197        assert_eq!(parse_interval_to_micros("abc"), None);
198    }
199}