nodedb_sql/resolver/expr/convert/
literals.rs1use 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 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 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 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 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 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 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 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 #[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 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 #[test]
177 fn double_quoted_string_value_unsupported() {
178 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 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 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}