use serde_json::Value;
use crate::response::ColumnType;
#[derive(Clone, Debug, PartialEq)]
pub enum CellValue {
Null,
Number(String),
Float(f64),
Bool(bool),
Text(String),
Date(i64),
Timestamp {
seconds: i64,
nanos: u32,
},
TimestampTz {
seconds: i64,
nanos: u32,
offset_minutes: i32,
},
Binary(Vec<u8>),
Json(Value),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WireError {
pub column: String,
pub snowflake_type: String,
pub reason: &'static str,
}
impl std::fmt::Display for WireError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"jsonv2 decode error in column {:?} ({}): {}",
self.column, self.snowflake_type, self.reason
)
}
}
impl std::error::Error for WireError {}
pub fn decode_cell(raw: Option<&str>, column: &ColumnType) -> Result<CellValue, WireError> {
let Some(text) = raw else {
return Ok(CellValue::Null);
};
let make_err = |reason: &'static str| WireError {
column: column.name.clone(),
snowflake_type: column.column_type.clone(),
reason,
};
let base_type = column
.column_type
.split('(')
.next()
.unwrap_or(&column.column_type)
.trim();
match base_type.to_ascii_uppercase().as_str() {
"FIXED" | "NUMBER" | "DECIMAL" | "NUMERIC" | "DECFLOAT" | "INT" | "INTEGER" | "BIGINT"
| "SMALLINT" | "TINYINT" | "BYTEINT" => Ok(CellValue::Number(text.to_owned())),
"REAL" | "FLOAT" | "FLOAT4" | "FLOAT8" | "DOUBLE" | "DOUBLE PRECISION" => text
.parse::<f64>()
.map(CellValue::Float)
.map_err(|_| make_err("expected a numeric REAL/FLOAT")),
"BOOLEAN" | "BOOL" => match text {
"true" => Ok(CellValue::Bool(true)),
"false" => Ok(CellValue::Bool(false)),
_ => Err(make_err("BOOLEAN must be the string \"true\" or \"false\"")),
},
"DATE" => text
.parse::<i64>()
.map(CellValue::Date)
.map_err(|_| make_err("DATE must be an integer epoch-day count")),
"TIME" | "TIMESTAMP_NTZ" | "TIMESTAMP_LTZ" | "DATETIME" => {
let (seconds, nanos) = parse_fractional_seconds(text)
.ok_or_else(|| make_err("expected fractional epoch seconds"))?;
Ok(CellValue::Timestamp { seconds, nanos })
}
"TIMESTAMP_TZ" => {
let (sec_part, offset_part) = text
.split_once(' ')
.ok_or_else(|| make_err("TIMESTAMP_TZ must be \"<seconds> <offset>\""))?;
let (seconds, nanos) = parse_fractional_seconds(sec_part)
.ok_or_else(|| make_err("expected fractional epoch seconds"))?;
let encoded_offset = offset_part
.parse::<i32>()
.map_err(|_| make_err("TIMESTAMP_TZ offset must be an integer"))?;
if !(720..=2160).contains(&encoded_offset) {
return Err(make_err("TIMESTAMP_TZ offset is out of range"));
}
Ok(CellValue::TimestampTz {
seconds,
nanos,
offset_minutes: encoded_offset - 1440,
})
}
"BINARY" | "VARBINARY" => decode_hex(text)
.map(CellValue::Binary)
.ok_or_else(|| make_err("BINARY must be an even-length hex string")),
"VARIANT" | "OBJECT" | "ARRAY" => serde_json::from_str(text)
.map(CellValue::Json)
.map_err(|_| make_err("VARIANT/OBJECT/ARRAY must hold embedded JSON")),
_ => Ok(CellValue::Text(text.to_owned())),
}
}
fn parse_fractional_seconds(text: &str) -> Option<(i64, u32)> {
let negative = text.starts_with('-');
let (int_str, frac_nanos) = match text.split_once('.') {
Some((int_str, frac)) => (int_str, frac_to_nanos(frac)?),
None => (text, 0),
};
let int_part = int_str.parse::<i64>().ok()?;
if !negative || frac_nanos == 0 {
Some((int_part, frac_nanos))
} else {
let seconds = int_part.checked_sub(1)?;
Some((seconds, 1_000_000_000 - frac_nanos))
}
}
fn frac_to_nanos(frac: &str) -> Option<u32> {
if frac.is_empty() || !frac.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let mut nanos = String::with_capacity(9);
nanos.extend(frac.chars().take(9));
while nanos.len() < 9 {
nanos.push('0');
}
nanos.parse::<u32>().ok()
}
fn decode_hex(text: &str) -> Option<Vec<u8>> {
let bytes = text.as_bytes();
if !bytes.len().is_multiple_of(2) {
return None;
}
bytes
.as_chunks::<2>()
.0
.iter()
.map(|pair| Some((hex_digit(pair[0])? << 4) | hex_digit(pair[1])?))
.collect()
}
fn hex_digit(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn col(snowflake_type: &str) -> ColumnType {
ColumnType {
name: "C".to_owned(),
column_type: snowflake_type.to_owned(),
scale: None,
precision: None,
nullable: true,
length: None,
byte_length: None,
database: None,
schema: None,
table: None,
collation: None,
}
}
#[test]
fn null_cell_decodes_to_null() -> Result<(), String> {
let value = decode_cell(None, &col("TEXT")).map_err(|e| e.to_string())?;
assert_eq!(value, CellValue::Null);
Ok(())
}
#[test]
fn number_is_kept_verbatim_without_scale_division() -> Result<(), String> {
let mut column = col("FIXED");
column.scale = Some(2);
let value = decode_cell(Some("1.50"), &column).map_err(|e| e.to_string())?;
assert_eq!(value, CellValue::Number("1.50".to_owned()));
assert_eq!(
decode_cell(
Some("1.2345678901234567890123456789012345678E+39"),
&col("DECFLOAT")
)
.map_err(|e| e.to_string())?,
CellValue::Number("1.2345678901234567890123456789012345678E+39".to_owned())
);
Ok(())
}
#[test]
fn boolean_is_string_not_json_bool() -> Result<(), String> {
assert_eq!(
decode_cell(Some("true"), &col("BOOLEAN")).map_err(|e| e.to_string())?,
CellValue::Bool(true)
);
assert_eq!(
decode_cell(Some("false"), &col("boolean")).map_err(|e| e.to_string())?,
CellValue::Bool(false)
);
assert!(decode_cell(Some("1"), &col("BOOLEAN")).is_err());
Ok(())
}
#[test]
fn date_is_epoch_days() -> Result<(), String> {
assert_eq!(
decode_cell(Some("18262"), &col("DATE")).map_err(|e| e.to_string())?,
CellValue::Date(18262)
);
assert!(decode_cell(Some("2020-01-01"), &col("DATE")).is_err());
Ok(())
}
#[test]
fn timestamp_is_fractional_epoch_seconds_not_nanos() -> Result<(), String> {
let value = decode_cell(Some("82919.000000000"), &col("TIMESTAMP_NTZ"))
.map_err(|e| e.to_string())?;
assert_eq!(
value,
CellValue::Timestamp {
seconds: 82919,
nanos: 0
}
);
let value = decode_cell(Some("100.5"), &col("TIME")).map_err(|e| e.to_string())?;
assert_eq!(
value,
CellValue::Timestamp {
seconds: 100,
nanos: 500_000_000
}
);
Ok(())
}
#[test]
fn timestamp_tz_decodes_offset_minus_1440() -> Result<(), String> {
let value = decode_cell(Some("1700000000.000000000 960"), &col("TIMESTAMP_TZ"))
.map_err(|e| e.to_string())?;
assert_eq!(
value,
CellValue::TimestampTz {
seconds: 1_700_000_000,
nanos: 0,
offset_minutes: -480
}
);
assert!(decode_cell(Some("1700000000.0"), &col("TIMESTAMP_TZ")).is_err());
assert_eq!(
decode_cell(Some("1700000000.0 720"), &col("TIMESTAMP_TZ"))
.map_err(|e| e.to_string())?,
CellValue::TimestampTz {
seconds: 1_700_000_000,
nanos: 0,
offset_minutes: -720
}
);
assert_eq!(
decode_cell(Some("1700000000.0 2160"), &col("TIMESTAMP_TZ"))
.map_err(|e| e.to_string())?,
CellValue::TimestampTz {
seconds: 1_700_000_000,
nanos: 0,
offset_minutes: 720
}
);
assert!(decode_cell(Some("1700000000.0 719"), &col("TIMESTAMP_TZ")).is_err());
assert!(decode_cell(Some("1700000000.0 2161"), &col("TIMESTAMP_TZ")).is_err());
Ok(())
}
#[test]
fn negative_pre_1970_timestamps_decode_with_borrow() -> Result<(), String> {
let cases: &[(&str, i64, u32)] = &[
("-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), ];
for (raw, seconds, nanos) in cases {
let value = decode_cell(Some(raw), &col("TIMESTAMP_NTZ")).map_err(|e| e.to_string())?;
assert_eq!(
value,
CellValue::Timestamp {
seconds: *seconds,
nanos: *nanos,
},
"decode of {raw:?}"
);
}
assert_eq!(
decode_cell(Some("1.5"), &col("TIMESTAMP_NTZ")).map_err(|e| e.to_string())?,
CellValue::Timestamp {
seconds: 1,
nanos: 500_000_000
}
);
Ok(())
}
#[test]
fn negative_timestamp_tz_decodes_with_borrow() -> Result<(), String> {
let value =
decode_cell(Some("-0.5 960"), &col("TIMESTAMP_TZ")).map_err(|e| e.to_string())?;
assert_eq!(
value,
CellValue::TimestampTz {
seconds: -1,
nanos: 500_000_000,
offset_minutes: -480,
}
);
Ok(())
}
#[test]
fn binary_is_hex_decoded() -> Result<(), String> {
assert_eq!(
decode_cell(Some("deadBEEF"), &col("BINARY")).map_err(|e| e.to_string())?,
CellValue::Binary(vec![0xde, 0xad, 0xbe, 0xef])
);
assert!(decode_cell(Some("abc"), &col("BINARY")).is_err()); assert!(decode_cell(Some("zz"), &col("BINARY")).is_err()); Ok(())
}
#[test]
fn variant_preserves_embedded_json() -> Result<(), String> {
let value =
decode_cell(Some(r#"{"k":[1,2]}"#), &col("VARIANT")).map_err(|e| e.to_string())?;
match value {
CellValue::Json(json) => assert_eq!(json["k"][1], serde_json::json!(2)),
other => return Err(format!("expected Json, got {other:?}")),
}
Ok(())
}
#[test]
fn unknown_type_falls_back_to_text() -> Result<(), String> {
assert_eq!(
decode_cell(Some("hello"), &col("GEOGRAPHY")).map_err(|e| e.to_string())?,
CellValue::Text("hello".to_owned())
);
Ok(())
}
#[test]
fn parameterized_types_decode_correctly() -> Result<(), String> {
assert_eq!(
decode_cell(Some("42"), &col("NUMBER(38,0)")).map_err(|e| e.to_string())?,
CellValue::Number("42".to_owned())
);
assert_eq!(
decode_cell(Some("3.14"), &col("DECIMAL(10, 2)")).map_err(|e| e.to_string())?,
CellValue::Number("3.14".to_owned())
);
assert_eq!(
decode_cell(Some("test"), &col("VARCHAR(255)")).map_err(|e| e.to_string())?,
CellValue::Text("test".to_owned())
);
Ok(())
}
}