1use std::collections::HashMap;
9
10use nodedb_sql::types::SqlValue;
11use nodedb_types::columnar::{ColumnDef, ColumnType};
12use nodedb_types::datetime::NdbDateTime;
13use nodedb_types::value::Value;
14
15use crate::error::LiteError;
16pub(crate) use crate::query::filter_convert::sql_value_to_value;
17
18pub fn build_row(
23 pairs: &[(String, SqlValue)],
24 columns: &[ColumnDef],
25) -> Result<Vec<Value>, LiteError> {
26 let pair_map: HashMap<&str, &SqlValue> = pairs.iter().map(|(k, v)| (k.as_str(), v)).collect();
27
28 let mut values = Vec::with_capacity(columns.len());
29 for col in columns {
30 let value = match pair_map.get(col.name.as_str()).copied() {
31 Some(v) => coerce_sql_value(v, &col.column_type)?,
32 None => Value::Null,
33 };
34 values.push(value);
35 }
36 Ok(values)
37}
38
39pub fn coerce_sql_value(v: &SqlValue, col_type: &ColumnType) -> Result<Value, LiteError> {
46 if matches!(v, SqlValue::Null) {
47 return Ok(Value::Null);
48 }
49 match col_type {
50 ColumnType::Timestamp => coerce_instant(v, col_type).map(Value::NaiveDateTime),
51 ColumnType::Timestamptz => coerce_instant(v, col_type).map(Value::DateTime),
52 ColumnType::Int64 => match v {
53 SqlValue::Int(i) => Ok(Value::Integer(*i)),
54 other => sql_value_to_value(other),
55 },
56 ColumnType::Float64 => match v {
57 SqlValue::Float(f) => Ok(Value::Float(*f)),
58 SqlValue::Int(i) => Ok(Value::Float(*i as f64)),
59 other => sql_value_to_value(other),
60 },
61 ColumnType::String => match v {
62 SqlValue::String(s) => Ok(Value::String(s.clone())),
63 other => sql_value_to_value(other),
64 },
65 ColumnType::Uuid => match v {
66 SqlValue::String(s) => Ok(Value::Uuid(s.clone())),
67 other => sql_value_to_value(other),
68 },
69 ColumnType::Bool => match v {
70 SqlValue::Bool(b) => Ok(Value::Bool(*b)),
71 other => sql_value_to_value(other),
72 },
73 ColumnType::Decimal(_) => match v {
74 SqlValue::Decimal(d) => Ok(Value::Decimal(*d)),
75 SqlValue::Int(i) => Ok(Value::Decimal(rust_decimal::Decimal::from(*i))),
76 SqlValue::String(s) => Ok(Value::String(s.clone())),
77 other => sql_value_to_value(other),
78 },
79 ColumnType::Bytes
80 | ColumnType::SystemTimestamp
81 | ColumnType::Geometry
82 | ColumnType::Vector(_)
83 | ColumnType::SparseVector
84 | ColumnType::Json
85 | ColumnType::Ulid
86 | ColumnType::Duration
87 | ColumnType::Array => sql_value_to_value(v),
88 other => Err(LiteError::Unsupported {
89 detail: format!("column type {other} has no write coercion rule in Lite"),
90 }),
91 }
92}
93
94pub fn coerce_value(v: Value, col_type: &ColumnType) -> Result<Value, LiteError> {
101 let literal = match &v {
102 Value::Null => return Ok(Value::Null),
103 Value::Integer(i) => SqlValue::Int(*i),
104 Value::Float(f) => SqlValue::Float(*f),
105 Value::Decimal(d) => SqlValue::Decimal(*d),
106 Value::String(s) => SqlValue::String(s.clone()),
107 Value::Bool(b) => SqlValue::Bool(*b),
108 Value::Bytes(b) => SqlValue::Bytes(b.clone()),
109 Value::NaiveDateTime(at) => SqlValue::Timestamp(*at),
110 Value::DateTime(at) => SqlValue::Timestamptz(*at),
111 _ => return Ok(v),
112 };
113 coerce_sql_value(&literal, col_type)
114}
115
116fn coerce_instant(v: &SqlValue, col_type: &ColumnType) -> Result<NdbDateTime, LiteError> {
121 let refused = |literal: String| LiteError::BadRequest {
122 detail: format!("{literal} is not representable as {col_type}"),
123 };
124 match v {
125 SqlValue::Timestamp(at) | SqlValue::Timestamptz(at) => Ok(*at),
126 SqlValue::String(s) => NdbDateTime::parse(s).ok_or_else(|| refused(format!("'{s}'"))),
127 SqlValue::Int(millis) => {
128 NdbDateTime::from_millis(*millis).map_err(|_| refused(millis.to_string()))
129 }
130 SqlValue::Float(f) => {
131 let whole = f.trunc();
132 if !whole.is_finite() || whole < i64::MIN as f64 || whole >= i64::MAX as f64 {
133 return Err(refused(f.to_string()));
134 }
135 NdbDateTime::from_millis(whole as i64).map_err(|_| refused(f.to_string()))
136 }
137 SqlValue::Decimal(d) => d
138 .trunc()
139 .to_string()
140 .parse::<i64>()
141 .ok()
142 .and_then(|millis| NdbDateTime::from_millis(millis).ok())
143 .ok_or_else(|| refused(d.to_string())),
144 SqlValue::Null => Err(refused("NULL".to_string())),
145 SqlValue::Bool(b) => Err(refused(b.to_string())),
146 SqlValue::Bytes(_) => Err(refused("a byte string".to_string())),
147 SqlValue::Array(_) => Err(refused("an array".to_string())),
148 }
149}
150
151pub fn sql_value_to_string(v: &SqlValue) -> String {
153 match v {
154 SqlValue::String(s) => s.clone(),
155 SqlValue::Int(i) => i.to_string(),
156 SqlValue::Float(f) => f.to_string(),
157 SqlValue::Bool(b) => b.to_string(),
158 SqlValue::Decimal(d) => d.to_string(),
159 SqlValue::Timestamp(at) | SqlValue::Timestamptz(at) => at.to_iso8601(),
160 SqlValue::Null | SqlValue::Bytes(_) | SqlValue::Array(_) => String::new(),
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 const NEW_YEAR_MICROS: i64 = 1_704_067_200_000_000;
170
171 fn at() -> NdbDateTime {
172 NdbDateTime::from_micros(NEW_YEAR_MICROS)
173 }
174
175 #[test]
178 fn typed_instant_lands_in_timestamp_column() {
179 assert_eq!(
180 coerce_sql_value(&SqlValue::Timestamp(at()), &ColumnType::Timestamp).expect("coerce"),
181 Value::NaiveDateTime(at())
182 );
183 assert_eq!(
184 coerce_sql_value(&SqlValue::Timestamptz(at()), &ColumnType::Timestamp).expect("coerce"),
185 Value::NaiveDateTime(at())
186 );
187 assert_eq!(
188 coerce_sql_value(&SqlValue::Timestamp(at()), &ColumnType::Timestamptz).expect("coerce"),
189 Value::DateTime(at())
190 );
191 }
192
193 #[test]
195 fn text_and_millis_read_as_instants() {
196 assert_eq!(
197 coerce_sql_value(
198 &SqlValue::String("2024-01-01 00:00:00".into()),
199 &ColumnType::Timestamp
200 )
201 .expect("coerce"),
202 Value::NaiveDateTime(at())
203 );
204 assert_eq!(
205 coerce_sql_value(
206 &SqlValue::Int(NEW_YEAR_MICROS / 1_000),
207 &ColumnType::Timestamptz
208 )
209 .expect("coerce"),
210 Value::DateTime(at())
211 );
212 assert_eq!(
213 coerce_sql_value(
214 &SqlValue::Float(NEW_YEAR_MICROS as f64 / 1_000.0 + 0.7),
215 &ColumnType::Timestamp
216 )
217 .expect("coerce"),
218 Value::NaiveDateTime(at())
219 );
220 }
221
222 #[test]
224 fn unreadable_instant_is_refused() {
225 for bad in [
226 SqlValue::String("not a date".into()),
227 SqlValue::Bool(true),
228 SqlValue::Int(i64::MAX),
229 ] {
230 let err = coerce_sql_value(&bad, &ColumnType::Timestamp).expect_err("refused");
231 assert!(matches!(err, LiteError::BadRequest { .. }), "{err:?}");
232 }
233 assert_eq!(
234 coerce_sql_value(&SqlValue::Null, &ColumnType::Timestamp).expect("null"),
235 Value::Null
236 );
237 }
238
239 #[test]
242 fn decimal_column_stores_the_exact_decimal() {
243 let typmod = nodedb_types::columnar::DecimalTypmod::new(5, 2).expect("typmod");
244 let exact = rust_decimal::Decimal::new(101, 2);
245 assert_eq!(
246 coerce_sql_value(
247 &SqlValue::Decimal(exact),
248 &ColumnType::Decimal(Some(typmod))
249 )
250 .expect("coerce"),
251 Value::Decimal(exact)
252 );
253 assert_eq!(
254 coerce_sql_value(&SqlValue::Int(7), &ColumnType::Decimal(None)).expect("coerce"),
255 Value::Decimal(rust_decimal::Decimal::from(7))
256 );
257 }
258
259 #[test]
262 fn build_row_orders_by_schema() {
263 let columns = vec![
264 ColumnDef::required("id", ColumnType::Int64),
265 ColumnDef::required("ts", ColumnType::Timestamp),
266 ColumnDef::nullable("value", ColumnType::Float64),
267 ];
268 let pairs = vec![
269 ("ts".to_string(), SqlValue::Timestamp(at())),
270 ("id".to_string(), SqlValue::Int(1)),
271 ];
272 let row = build_row(&pairs, &columns).expect("row");
273 assert_eq!(
274 row,
275 vec![Value::Integer(1), Value::NaiveDateTime(at()), Value::Null]
276 );
277 }
278
279 #[test]
282 fn evaluated_value_coerces_like_its_literal() {
283 assert_eq!(
284 coerce_value(Value::Integer(3), &ColumnType::Float64).expect("coerce"),
285 Value::Float(3.0)
286 );
287 assert_eq!(
288 coerce_value(Value::String("a-b".into()), &ColumnType::Uuid).expect("coerce"),
289 Value::Uuid("a-b".into())
290 );
291 let array = Value::Array(vec![Value::Float(1.0)]);
292 assert_eq!(
293 coerce_value(array.clone(), &ColumnType::Array).expect("coerce"),
294 array
295 );
296 }
297}