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/// - Pure integers → `SqlValue::Int`.
12/// - Numbers with `.`, `e`, or `E` → `SqlValue::Decimal` (exact arithmetic).
13/// - If decimal parse fails → fallback to `SqlValue::Float`, then `SqlValue::String`.
14pub fn convert_value(val: &Value) -> Result<SqlValue> {
15    match val {
16        Value::Number(n, _) => {
17            if let Ok(i) = n.parse::<i64>() {
18                Ok(SqlValue::Int(i))
19            } else if n.contains('.') || n.contains('e') || n.contains('E') {
20                // Fractional or scientific notation: prefer exact Decimal.
21                if let Ok(d) = rust_decimal::Decimal::from_str_exact(n) {
22                    Ok(SqlValue::Decimal(d))
23                } else if let Ok(f) = n.parse::<f64>() {
24                    Ok(SqlValue::Float(f))
25                } else {
26                    Ok(SqlValue::String(n.clone()))
27                }
28            } else if let Ok(f) = n.parse::<f64>() {
29                Ok(SqlValue::Float(f))
30            } else {
31                Ok(SqlValue::String(n.clone()))
32            }
33        }
34        Value::SingleQuotedString(s) => Ok(SqlValue::String(s.clone())),
35        Value::Boolean(b) => Ok(SqlValue::Bool(*b)),
36        Value::Null => Ok(SqlValue::Null),
37        // `X'...'` hex-string literal → raw bytes. Enables byte-typed
38        // arguments such as WKB geometry (`ST_GeomFromWKB(X'01...')`).
39        Value::HexStringLiteral(s) => Ok(SqlValue::Bytes(decode_hex_literal(s)?)),
40        _ => Err(SqlError::Unsupported {
41            detail: format!("value literal: {val}"),
42        }),
43    }
44}
45
46/// Decode an even-length ASCII hex string (the inner text of an `X'...'`
47/// literal) into its byte sequence.
48fn decode_hex_literal(s: &str) -> Result<Vec<u8>> {
49    parse_hex_bytes(s).map_err(|reason| SqlError::Parse {
50        detail: format!("hex literal {reason}: X'{s}'"),
51    })
52}
53
54/// Parse an even-length ASCII hex string into bytes. Shared by `X'...'`
55/// literal decoding here and plain hex-string WKB arguments in
56/// `planner::spatial_ctor`, so the two surfaces never drift. Returns the
57/// failure reason as a fragment; callers wrap it into their own contextual
58/// `SqlError` variant.
59pub(crate) fn parse_hex_bytes(s: &str) -> std::result::Result<Vec<u8>, &'static str> {
60    if !s.len().is_multiple_of(2) {
61        return Err("must have an even number of digits");
62    }
63    (0..s.len())
64        .step_by(2)
65        .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|_| "contains an invalid hex digit"))
66        .collect()
67}
68
69/// Parse an interval string to microseconds.
70///
71/// Delegates to `nodedb_types::kv_parsing::parse_interval_to_ms` (ms → μs)
72/// and `NdbDuration::parse` for compound shorthand forms.
73pub(super) fn parse_interval_to_micros(s: &str) -> Option<i64> {
74    let s = s.trim();
75    if s.is_empty() {
76        return None;
77    }
78
79    // Try NdbDuration::parse first (handles compound "1h30m", "500ms", "2d").
80    if let Some(dur) = nodedb_types::NdbDuration::parse(s) {
81        return Some(dur.micros);
82    }
83
84    // Delegate to shared interval parser (handles all forms including compound).
85    if let Ok(ms) = nodedb_types::kv_parsing::parse_interval_to_ms(s) {
86        return Some(ms as i64 * 1000); // ms → μs
87    }
88
89    None
90}