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 let base_type = column
114 .column_type
115 .split('(')
116 .next()
117 .unwrap_or(&column.column_type)
118 .trim();
119
120 match base_type.to_ascii_uppercase().as_str() {
121 "FIXED" | "NUMBER" | "DECIMAL" | "NUMERIC" | "DECFLOAT" | "INT" | "INTEGER" | "BIGINT"
122 | "SMALLINT" | "TINYINT" | "BYTEINT" => Ok(CellValue::Number(text.to_owned())),
123
124 "REAL" | "FLOAT" | "FLOAT4" | "FLOAT8" | "DOUBLE" | "DOUBLE PRECISION" => text
125 .parse::<f64>()
126 .map(CellValue::Float)
127 .map_err(|_| make_err("expected a numeric REAL/FLOAT")),
128
129 "BOOLEAN" | "BOOL" => match text {
130 "true" => Ok(CellValue::Bool(true)),
131 "false" => Ok(CellValue::Bool(false)),
132 _ => Err(make_err("BOOLEAN must be the string \"true\" or \"false\"")),
133 },
134
135 "DATE" => text
136 .parse::<i64>()
137 .map(CellValue::Date)
138 .map_err(|_| make_err("DATE must be an integer epoch-day count")),
139
140 "TIME" | "TIMESTAMP_NTZ" | "TIMESTAMP_LTZ" | "DATETIME" => {
141 let (seconds, nanos) = parse_fractional_seconds(text)
142 .ok_or_else(|| make_err("expected fractional epoch seconds"))?;
143 Ok(CellValue::Timestamp { seconds, nanos })
144 }
145
146 "TIMESTAMP_TZ" => {
147 let (sec_part, offset_part) = text
148 .split_once(' ')
149 .ok_or_else(|| make_err("TIMESTAMP_TZ must be \"<seconds> <offset>\""))?;
150 let (seconds, nanos) = parse_fractional_seconds(sec_part)
151 .ok_or_else(|| make_err("expected fractional epoch seconds"))?;
152 let encoded_offset = offset_part
153 .parse::<i32>()
154 .map_err(|_| make_err("TIMESTAMP_TZ offset must be an integer"))?;
155 if !(720..=2160).contains(&encoded_offset) {
158 return Err(make_err("TIMESTAMP_TZ offset is out of range"));
159 }
160 Ok(CellValue::TimestampTz {
161 seconds,
162 nanos,
163 offset_minutes: encoded_offset - 1440,
164 })
165 }
166
167 "BINARY" | "VARBINARY" => decode_hex(text)
168 .map(CellValue::Binary)
169 .ok_or_else(|| make_err("BINARY must be an even-length hex string")),
170
171 "VARIANT" | "OBJECT" | "ARRAY" => serde_json::from_str(text)
172 .map(CellValue::Json)
173 .map_err(|_| make_err("VARIANT/OBJECT/ARRAY must hold embedded JSON")),
174
175 _ => Ok(CellValue::Text(text.to_owned())),
177 }
178}
179
180fn parse_fractional_seconds(text: &str) -> Option<(i64, u32)> {
190 let negative = text.starts_with('-');
191 let (int_str, frac_nanos) = match text.split_once('.') {
192 Some((int_str, frac)) => (int_str, frac_to_nanos(frac)?),
193 None => (text, 0),
194 };
195 let int_part = int_str.parse::<i64>().ok()?;
196 if !negative || frac_nanos == 0 {
197 Some((int_part, frac_nanos))
199 } else {
200 let seconds = int_part.checked_sub(1)?;
204 Some((seconds, 1_000_000_000 - frac_nanos))
205 }
206}
207
208fn frac_to_nanos(frac: &str) -> Option<u32> {
211 if frac.is_empty() || !frac.bytes().all(|b| b.is_ascii_digit()) {
212 return None;
213 }
214 let mut nanos = String::with_capacity(9);
215 nanos.extend(frac.chars().take(9));
216 while nanos.len() < 9 {
217 nanos.push('0');
218 }
219 nanos.parse::<u32>().ok()
220}
221
222fn decode_hex(text: &str) -> Option<Vec<u8>> {
225 let bytes = text.as_bytes();
226 if !bytes.len().is_multiple_of(2) {
227 return None;
228 }
229 bytes
230 .as_chunks::<2>()
231 .0
232 .iter()
233 .map(|pair| Some((hex_digit(pair[0])? << 4) | hex_digit(pair[1])?))
234 .collect()
235}
236
237fn hex_digit(byte: u8) -> Option<u8> {
239 match byte {
240 b'0'..=b'9' => Some(byte - b'0'),
241 b'a'..=b'f' => Some(byte - b'a' + 10),
242 b'A'..=b'F' => Some(byte - b'A' + 10),
243 _ => None,
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 fn col(snowflake_type: &str) -> ColumnType {
252 ColumnType {
253 name: "C".to_owned(),
254 column_type: snowflake_type.to_owned(),
255 scale: None,
256 precision: None,
257 nullable: true,
258 length: None,
259 byte_length: None,
260 database: None,
261 schema: None,
262 table: None,
263 collation: None,
264 }
265 }
266
267 #[test]
268 fn null_cell_decodes_to_null() -> Result<(), String> {
269 let value = decode_cell(None, &col("TEXT")).map_err(|e| e.to_string())?;
270 assert_eq!(value, CellValue::Null);
271 Ok(())
272 }
273
274 #[test]
275 fn number_is_kept_verbatim_without_scale_division() -> Result<(), String> {
276 let mut column = col("FIXED");
278 column.scale = Some(2);
279 let value = decode_cell(Some("1.50"), &column).map_err(|e| e.to_string())?;
280 assert_eq!(value, CellValue::Number("1.50".to_owned()));
281 assert_eq!(
282 decode_cell(
283 Some("1.2345678901234567890123456789012345678E+39"),
284 &col("DECFLOAT")
285 )
286 .map_err(|e| e.to_string())?,
287 CellValue::Number("1.2345678901234567890123456789012345678E+39".to_owned())
288 );
289 Ok(())
290 }
291
292 #[test]
293 fn boolean_is_string_not_json_bool() -> Result<(), String> {
294 assert_eq!(
295 decode_cell(Some("true"), &col("BOOLEAN")).map_err(|e| e.to_string())?,
296 CellValue::Bool(true)
297 );
298 assert_eq!(
299 decode_cell(Some("false"), &col("boolean")).map_err(|e| e.to_string())?,
300 CellValue::Bool(false)
301 );
302 assert!(decode_cell(Some("1"), &col("BOOLEAN")).is_err());
304 Ok(())
305 }
306
307 #[test]
308 fn date_is_epoch_days() -> Result<(), String> {
309 assert_eq!(
311 decode_cell(Some("18262"), &col("DATE")).map_err(|e| e.to_string())?,
312 CellValue::Date(18262)
313 );
314 assert!(decode_cell(Some("2020-01-01"), &col("DATE")).is_err());
315 Ok(())
316 }
317
318 #[test]
319 fn timestamp_is_fractional_epoch_seconds_not_nanos() -> Result<(), String> {
320 let value = decode_cell(Some("82919.000000000"), &col("TIMESTAMP_NTZ"))
321 .map_err(|e| e.to_string())?;
322 assert_eq!(
323 value,
324 CellValue::Timestamp {
325 seconds: 82919,
326 nanos: 0
327 }
328 );
329 let value = decode_cell(Some("100.5"), &col("TIME")).map_err(|e| e.to_string())?;
331 assert_eq!(
332 value,
333 CellValue::Timestamp {
334 seconds: 100,
335 nanos: 500_000_000
336 }
337 );
338 Ok(())
339 }
340
341 #[test]
342 fn timestamp_tz_decodes_offset_minus_1440() -> Result<(), String> {
343 let value = decode_cell(Some("1700000000.000000000 960"), &col("TIMESTAMP_TZ"))
348 .map_err(|e| e.to_string())?;
349 assert_eq!(
350 value,
351 CellValue::TimestampTz {
352 seconds: 1_700_000_000,
353 nanos: 0,
354 offset_minutes: -480
355 }
356 );
357 assert!(decode_cell(Some("1700000000.0"), &col("TIMESTAMP_TZ")).is_err());
358 assert_eq!(
359 decode_cell(Some("1700000000.0 720"), &col("TIMESTAMP_TZ"))
360 .map_err(|e| e.to_string())?,
361 CellValue::TimestampTz {
362 seconds: 1_700_000_000,
363 nanos: 0,
364 offset_minutes: -720
365 }
366 );
367 assert_eq!(
368 decode_cell(Some("1700000000.0 2160"), &col("TIMESTAMP_TZ"))
369 .map_err(|e| e.to_string())?,
370 CellValue::TimestampTz {
371 seconds: 1_700_000_000,
372 nanos: 0,
373 offset_minutes: 720
374 }
375 );
376 assert!(decode_cell(Some("1700000000.0 719"), &col("TIMESTAMP_TZ")).is_err());
377 assert!(decode_cell(Some("1700000000.0 2161"), &col("TIMESTAMP_TZ")).is_err());
378 Ok(())
379 }
380
381 #[test]
382 fn negative_pre_1970_timestamps_decode_with_borrow() -> Result<(), String> {
383 let cases: &[(&str, i64, u32)] = &[
387 ("-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), ];
393 for (raw, seconds, nanos) in cases {
394 let value = decode_cell(Some(raw), &col("TIMESTAMP_NTZ")).map_err(|e| e.to_string())?;
395 assert_eq!(
396 value,
397 CellValue::Timestamp {
398 seconds: *seconds,
399 nanos: *nanos,
400 },
401 "decode of {raw:?}"
402 );
403 }
404 assert_eq!(
406 decode_cell(Some("1.5"), &col("TIMESTAMP_NTZ")).map_err(|e| e.to_string())?,
407 CellValue::Timestamp {
408 seconds: 1,
409 nanos: 500_000_000
410 }
411 );
412 Ok(())
413 }
414
415 #[test]
416 fn negative_timestamp_tz_decodes_with_borrow() -> Result<(), String> {
417 let value =
419 decode_cell(Some("-0.5 960"), &col("TIMESTAMP_TZ")).map_err(|e| e.to_string())?;
420 assert_eq!(
421 value,
422 CellValue::TimestampTz {
423 seconds: -1,
424 nanos: 500_000_000,
425 offset_minutes: -480,
426 }
427 );
428 Ok(())
429 }
430
431 #[test]
432 fn binary_is_hex_decoded() -> Result<(), String> {
433 assert_eq!(
434 decode_cell(Some("deadBEEF"), &col("BINARY")).map_err(|e| e.to_string())?,
435 CellValue::Binary(vec![0xde, 0xad, 0xbe, 0xef])
436 );
437 assert!(decode_cell(Some("abc"), &col("BINARY")).is_err()); assert!(decode_cell(Some("zz"), &col("BINARY")).is_err()); Ok(())
440 }
441
442 #[test]
443 fn variant_preserves_embedded_json() -> Result<(), String> {
444 let value =
445 decode_cell(Some(r#"{"k":[1,2]}"#), &col("VARIANT")).map_err(|e| e.to_string())?;
446 match value {
447 CellValue::Json(json) => assert_eq!(json["k"][1], serde_json::json!(2)),
448 other => return Err(format!("expected Json, got {other:?}")),
449 }
450 Ok(())
451 }
452
453 #[test]
454 fn unknown_type_falls_back_to_text() -> Result<(), String> {
455 assert_eq!(
456 decode_cell(Some("hello"), &col("GEOGRAPHY")).map_err(|e| e.to_string())?,
457 CellValue::Text("hello".to_owned())
458 );
459 Ok(())
460 }
461
462 #[test]
463 fn parameterized_types_decode_correctly() -> Result<(), String> {
464 assert_eq!(
465 decode_cell(Some("42"), &col("NUMBER(38,0)")).map_err(|e| e.to_string())?,
466 CellValue::Number("42".to_owned())
467 );
468 assert_eq!(
469 decode_cell(Some("3.14"), &col("DECIMAL(10, 2)")).map_err(|e| e.to_string())?,
470 CellValue::Number("3.14".to_owned())
471 );
472 assert_eq!(
473 decode_cell(Some("test"), &col("VARCHAR(255)")).map_err(|e| e.to_string())?,
474 CellValue::Text("test".to_owned())
475 );
476 Ok(())
477 }
478}