1use serde_json::Value;
27
28use crate::response::ColumnType;
29
30#[derive(Clone, Debug, PartialEq)]
35pub enum CellValue {
36 Null,
38 Number(String),
40 Float(f64),
42 Bool(bool),
44 Text(String),
46 Date(i64),
48 Timestamp {
51 seconds: i64,
53 nanos: u32,
55 },
56 TimestampTz {
59 seconds: i64,
61 nanos: u32,
63 offset_minutes: i32,
65 },
66 Binary(Vec<u8>),
68 Json(Value),
70}
71
72#[derive(Clone, Debug, PartialEq, Eq)]
76pub struct WireError {
77 pub column: String,
79 pub snowflake_type: String,
81 pub reason: &'static str,
83}
84
85impl std::fmt::Display for WireError {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 write!(
88 f,
89 "jsonv2 decode error in column {:?} ({}): {}",
90 self.column, self.snowflake_type, self.reason
91 )
92 }
93}
94
95impl std::error::Error for WireError {}
96
97pub fn decode_cell(raw: Option<&str>, column: &ColumnType) -> Result<CellValue, WireError> {
104 let Some(text) = raw else {
105 return Ok(CellValue::Null);
106 };
107 let make_err = |reason: &'static str| WireError {
108 column: column.name.clone(),
109 snowflake_type: column.column_type.clone(),
110 reason,
111 };
112
113 match column.column_type.to_ascii_uppercase().as_str() {
114 "FIXED" | "NUMBER" | "DECIMAL" | "NUMERIC" | "DECFLOAT" | "INT" | "INTEGER" | "BIGINT"
115 | "SMALLINT" | "TINYINT" | "BYTEINT" => Ok(CellValue::Number(text.to_owned())),
116
117 "REAL" | "FLOAT" | "FLOAT4" | "FLOAT8" | "DOUBLE" | "DOUBLE PRECISION" => text
118 .parse::<f64>()
119 .map(CellValue::Float)
120 .map_err(|_| make_err("expected a numeric REAL/FLOAT")),
121
122 "BOOLEAN" | "BOOL" => match text {
123 "true" => Ok(CellValue::Bool(true)),
124 "false" => Ok(CellValue::Bool(false)),
125 _ => Err(make_err("BOOLEAN must be the string \"true\" or \"false\"")),
126 },
127
128 "DATE" => text
129 .parse::<i64>()
130 .map(CellValue::Date)
131 .map_err(|_| make_err("DATE must be an integer epoch-day count")),
132
133 "TIME" | "TIMESTAMP_NTZ" | "TIMESTAMP_LTZ" | "DATETIME" => {
134 let (seconds, nanos) = parse_fractional_seconds(text)
135 .ok_or_else(|| make_err("expected fractional epoch seconds"))?;
136 Ok(CellValue::Timestamp { seconds, nanos })
137 }
138
139 "TIMESTAMP_TZ" => {
140 let (sec_part, offset_part) = text
141 .split_once(' ')
142 .ok_or_else(|| make_err("TIMESTAMP_TZ must be \"<seconds> <offset>\""))?;
143 let (seconds, nanos) = parse_fractional_seconds(sec_part)
144 .ok_or_else(|| make_err("expected fractional epoch seconds"))?;
145 let encoded_offset = offset_part
146 .parse::<i32>()
147 .map_err(|_| make_err("TIMESTAMP_TZ offset must be an integer"))?;
148 if !(720..=2160).contains(&encoded_offset) {
151 return Err(make_err("TIMESTAMP_TZ offset is out of range"));
152 }
153 Ok(CellValue::TimestampTz {
154 seconds,
155 nanos,
156 offset_minutes: encoded_offset - 1440,
157 })
158 }
159
160 "BINARY" | "VARBINARY" => decode_hex(text)
161 .map(CellValue::Binary)
162 .ok_or_else(|| make_err("BINARY must be an even-length hex string")),
163
164 "VARIANT" | "OBJECT" | "ARRAY" => serde_json::from_str(text)
165 .map(CellValue::Json)
166 .map_err(|_| make_err("VARIANT/OBJECT/ARRAY must hold embedded JSON")),
167
168 _ => Ok(CellValue::Text(text.to_owned())),
170 }
171}
172
173fn parse_fractional_seconds(text: &str) -> Option<(i64, u32)> {
183 let negative = text.starts_with('-');
184 let (int_str, frac_nanos) = match text.split_once('.') {
185 Some((int_str, frac)) => (int_str, frac_to_nanos(frac)?),
186 None => (text, 0),
187 };
188 let int_part = int_str.parse::<i64>().ok()?;
189 if !negative || frac_nanos == 0 {
190 Some((int_part, frac_nanos))
192 } else {
193 let seconds = int_part.checked_sub(1)?;
197 Some((seconds, 1_000_000_000 - frac_nanos))
198 }
199}
200
201fn frac_to_nanos(frac: &str) -> Option<u32> {
204 if frac.is_empty() || !frac.bytes().all(|b| b.is_ascii_digit()) {
205 return None;
206 }
207 let mut nanos = String::with_capacity(9);
208 nanos.extend(frac.chars().take(9));
209 while nanos.len() < 9 {
210 nanos.push('0');
211 }
212 nanos.parse::<u32>().ok()
213}
214
215fn decode_hex(text: &str) -> Option<Vec<u8>> {
218 let bytes = text.as_bytes();
219 if !bytes.len().is_multiple_of(2) {
220 return None;
221 }
222 bytes
223 .as_chunks::<2>()
224 .0
225 .iter()
226 .map(|pair| Some((hex_digit(pair[0])? << 4) | hex_digit(pair[1])?))
227 .collect()
228}
229
230fn hex_digit(byte: u8) -> Option<u8> {
232 match byte {
233 b'0'..=b'9' => Some(byte - b'0'),
234 b'a'..=b'f' => Some(byte - b'a' + 10),
235 b'A'..=b'F' => Some(byte - b'A' + 10),
236 _ => None,
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 fn col(snowflake_type: &str) -> ColumnType {
245 ColumnType {
246 name: "C".to_owned(),
247 column_type: snowflake_type.to_owned(),
248 scale: None,
249 precision: None,
250 nullable: true,
251 length: None,
252 byte_length: None,
253 database: None,
254 schema: None,
255 table: None,
256 collation: None,
257 }
258 }
259
260 #[test]
261 fn null_cell_decodes_to_null() -> Result<(), String> {
262 let value = decode_cell(None, &col("TEXT")).map_err(|e| e.to_string())?;
263 assert_eq!(value, CellValue::Null);
264 Ok(())
265 }
266
267 #[test]
268 fn number_is_kept_verbatim_without_scale_division() -> Result<(), String> {
269 let mut column = col("FIXED");
271 column.scale = Some(2);
272 let value = decode_cell(Some("1.50"), &column).map_err(|e| e.to_string())?;
273 assert_eq!(value, CellValue::Number("1.50".to_owned()));
274 assert_eq!(
275 decode_cell(
276 Some("1.2345678901234567890123456789012345678E+39"),
277 &col("DECFLOAT")
278 )
279 .map_err(|e| e.to_string())?,
280 CellValue::Number("1.2345678901234567890123456789012345678E+39".to_owned())
281 );
282 Ok(())
283 }
284
285 #[test]
286 fn boolean_is_string_not_json_bool() -> Result<(), String> {
287 assert_eq!(
288 decode_cell(Some("true"), &col("BOOLEAN")).map_err(|e| e.to_string())?,
289 CellValue::Bool(true)
290 );
291 assert_eq!(
292 decode_cell(Some("false"), &col("boolean")).map_err(|e| e.to_string())?,
293 CellValue::Bool(false)
294 );
295 assert!(decode_cell(Some("1"), &col("BOOLEAN")).is_err());
297 Ok(())
298 }
299
300 #[test]
301 fn date_is_epoch_days() -> Result<(), String> {
302 assert_eq!(
304 decode_cell(Some("18262"), &col("DATE")).map_err(|e| e.to_string())?,
305 CellValue::Date(18262)
306 );
307 assert!(decode_cell(Some("2020-01-01"), &col("DATE")).is_err());
308 Ok(())
309 }
310
311 #[test]
312 fn timestamp_is_fractional_epoch_seconds_not_nanos() -> Result<(), String> {
313 let value = decode_cell(Some("82919.000000000"), &col("TIMESTAMP_NTZ"))
314 .map_err(|e| e.to_string())?;
315 assert_eq!(
316 value,
317 CellValue::Timestamp {
318 seconds: 82919,
319 nanos: 0
320 }
321 );
322 let value = decode_cell(Some("100.5"), &col("TIME")).map_err(|e| e.to_string())?;
324 assert_eq!(
325 value,
326 CellValue::Timestamp {
327 seconds: 100,
328 nanos: 500_000_000
329 }
330 );
331 Ok(())
332 }
333
334 #[test]
335 fn timestamp_tz_decodes_offset_minus_1440() -> Result<(), String> {
336 let value = decode_cell(Some("1700000000.000000000 960"), &col("TIMESTAMP_TZ"))
341 .map_err(|e| e.to_string())?;
342 assert_eq!(
343 value,
344 CellValue::TimestampTz {
345 seconds: 1_700_000_000,
346 nanos: 0,
347 offset_minutes: -480
348 }
349 );
350 assert!(decode_cell(Some("1700000000.0"), &col("TIMESTAMP_TZ")).is_err());
351 assert_eq!(
352 decode_cell(Some("1700000000.0 720"), &col("TIMESTAMP_TZ"))
353 .map_err(|e| e.to_string())?,
354 CellValue::TimestampTz {
355 seconds: 1_700_000_000,
356 nanos: 0,
357 offset_minutes: -720
358 }
359 );
360 assert_eq!(
361 decode_cell(Some("1700000000.0 2160"), &col("TIMESTAMP_TZ"))
362 .map_err(|e| e.to_string())?,
363 CellValue::TimestampTz {
364 seconds: 1_700_000_000,
365 nanos: 0,
366 offset_minutes: 720
367 }
368 );
369 assert!(decode_cell(Some("1700000000.0 719"), &col("TIMESTAMP_TZ")).is_err());
370 assert!(decode_cell(Some("1700000000.0 2161"), &col("TIMESTAMP_TZ")).is_err());
371 Ok(())
372 }
373
374 #[test]
375 fn negative_pre_1970_timestamps_decode_with_borrow() -> Result<(), String> {
376 let cases: &[(&str, i64, u32)] = &[
380 ("-1.5", -2, 500_000_000), ("-0.5", -1, 500_000_000), ("-1.0", -1, 0), ("-1", -1, 0), ("-86400.250000000", -86401, 750_000_000), ];
386 for (raw, seconds, nanos) in cases {
387 let value = decode_cell(Some(raw), &col("TIMESTAMP_NTZ")).map_err(|e| e.to_string())?;
388 assert_eq!(
389 value,
390 CellValue::Timestamp {
391 seconds: *seconds,
392 nanos: *nanos,
393 },
394 "decode of {raw:?}"
395 );
396 }
397 assert_eq!(
399 decode_cell(Some("1.5"), &col("TIMESTAMP_NTZ")).map_err(|e| e.to_string())?,
400 CellValue::Timestamp {
401 seconds: 1,
402 nanos: 500_000_000
403 }
404 );
405 Ok(())
406 }
407
408 #[test]
409 fn negative_timestamp_tz_decodes_with_borrow() -> Result<(), String> {
410 let value =
412 decode_cell(Some("-0.5 960"), &col("TIMESTAMP_TZ")).map_err(|e| e.to_string())?;
413 assert_eq!(
414 value,
415 CellValue::TimestampTz {
416 seconds: -1,
417 nanos: 500_000_000,
418 offset_minutes: -480,
419 }
420 );
421 Ok(())
422 }
423
424 #[test]
425 fn binary_is_hex_decoded() -> Result<(), String> {
426 assert_eq!(
427 decode_cell(Some("deadBEEF"), &col("BINARY")).map_err(|e| e.to_string())?,
428 CellValue::Binary(vec![0xde, 0xad, 0xbe, 0xef])
429 );
430 assert!(decode_cell(Some("abc"), &col("BINARY")).is_err()); assert!(decode_cell(Some("zz"), &col("BINARY")).is_err()); Ok(())
433 }
434
435 #[test]
436 fn variant_preserves_embedded_json() -> Result<(), String> {
437 let value =
438 decode_cell(Some(r#"{"k":[1,2]}"#), &col("VARIANT")).map_err(|e| e.to_string())?;
439 match value {
440 CellValue::Json(json) => assert_eq!(json["k"][1], serde_json::json!(2)),
441 other => return Err(format!("expected Json, got {other:?}")),
442 }
443 Ok(())
444 }
445
446 #[test]
447 fn unknown_type_falls_back_to_text() -> Result<(), String> {
448 assert_eq!(
449 decode_cell(Some("hello"), &col("GEOGRAPHY")).map_err(|e| e.to_string())?,
450 CellValue::Text("hello".to_owned())
451 );
452 Ok(())
453 }
454}