faucet_source_snowflake/
convert.rs1use serde::Deserialize;
23use serde_json::{Map, Value};
24
25#[derive(Debug, Clone, Deserialize)]
28pub struct ColumnMeta {
29 pub name: String,
32 #[serde(rename = "type")]
37 pub ty: String,
38}
39
40pub fn row_to_json(row: &[Value], columns: &[ColumnMeta]) -> Value {
46 let mut obj = Map::with_capacity(columns.len());
47 for (i, col) in columns.iter().enumerate() {
48 let cell = row.get(i);
49 obj.insert(col.name.clone(), cell_to_json(cell, &col.ty));
50 }
51 Value::Object(obj)
52}
53
54fn cell_to_json(cell: Option<&Value>, ty: &str) -> Value {
56 let Some(cell) = cell else {
57 return Value::Null;
58 };
59 if cell.is_null() {
61 return Value::Null;
62 }
63 let s = match cell {
68 Value::String(s) => s.as_str(),
69 other => return other.clone(),
70 };
71
72 match ty.to_ascii_lowercase().as_str() {
73 "fixed" => parse_number(s),
74 "real" => parse_real(s),
75 "boolean" => parse_bool(s),
76 "variant" | "object" | "array" => {
77 serde_json::from_str(s).unwrap_or_else(|_| Value::String(s.to_owned()))
78 }
79 _ => Value::String(s.to_owned()),
80 }
81}
82
83fn parse_number(s: &str) -> Value {
92 if let Ok(i) = s.parse::<i64>() {
93 return Value::Number(i.into());
94 }
95 if let Ok(u) = s.parse::<u64>() {
96 return Value::Number(u.into());
97 }
98 let digits = s.trim().strip_prefix(['+', '-']).unwrap_or(s.trim());
101 if !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit()) {
102 return Value::String(s.to_owned());
103 }
104 parse_real(s)
105}
106
107fn parse_real(s: &str) -> Value {
110 match s.parse::<f64>() {
111 Ok(f) => serde_json::Number::from_f64(f)
112 .map(Value::Number)
113 .unwrap_or_else(|| Value::String(s.to_owned())),
114 Err(_) => Value::String(s.to_owned()),
115 }
116}
117
118fn parse_bool(s: &str) -> Value {
122 match s {
123 "true" | "TRUE" | "1" => Value::Bool(true),
124 "false" | "FALSE" | "0" => Value::Bool(false),
125 other => Value::String(other.to_owned()),
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use serde_json::json;
133
134 fn col(name: &str, ty: &str) -> ColumnMeta {
135 ColumnMeta {
136 name: name.into(),
137 ty: ty.into(),
138 }
139 }
140
141 #[test]
142 fn fixed_parses_as_integer() {
143 let row = [json!("42")];
144 let cols = [col("ID", "fixed")];
145 assert_eq!(row_to_json(&row, &cols), json!({"ID": 42}));
146 }
147
148 #[test]
149 fn fixed_decimal_falls_back_to_float() {
150 let row = [json!("2.5")];
151 let cols = [col("RATIO", "fixed")];
152 assert_eq!(row_to_json(&row, &cols), json!({"RATIO": 2.5}));
153 }
154
155 #[test]
156 fn fixed_past_i64_within_u64_stays_numeric() {
157 let row = [json!("18446744073709551615")]; let cols = [col("ID", "fixed")];
162 assert_eq!(
163 row_to_json(&row, &cols),
164 json!({"ID": 18446744073709551615u64})
165 );
166 }
167
168 #[test]
169 fn fixed_beyond_u64_kept_as_string_lossless() {
170 let row = [json!("123456789012345678901234567890")];
173 let cols = [col("ID", "fixed")];
174 assert_eq!(
175 row_to_json(&row, &cols),
176 json!({"ID": "123456789012345678901234567890"})
177 );
178 }
179
180 #[test]
181 fn real_parses_as_float() {
182 let row = [json!("0.5")];
183 let cols = [col("X", "real")];
184 assert_eq!(row_to_json(&row, &cols), json!({"X": 0.5}));
185 }
186
187 #[test]
188 fn boolean_parses_lowercase() {
189 let row = [json!("true"), json!("false")];
190 let cols = [col("A", "boolean"), col("B", "boolean")];
191 assert_eq!(row_to_json(&row, &cols), json!({"A": true, "B": false}));
192 }
193
194 #[test]
195 fn variant_parses_inner_json() {
196 let row = [json!(r#"{"k":1}"#)];
197 let cols = [col("DATA", "variant")];
198 assert_eq!(row_to_json(&row, &cols), json!({"DATA": {"k": 1}}));
199 }
200
201 #[test]
202 fn variant_falls_back_to_string_on_invalid_json() {
203 let row = [json!("not-json")];
204 let cols = [col("DATA", "variant")];
205 assert_eq!(row_to_json(&row, &cols), json!({"DATA": "not-json"}));
206 }
207
208 #[test]
209 fn text_passes_through() {
210 let row = [json!("hello")];
211 let cols = [col("NAME", "text")];
212 assert_eq!(row_to_json(&row, &cols), json!({"NAME": "hello"}));
213 }
214
215 #[test]
216 fn null_cell_maps_to_null() {
217 let row = [json!(null)];
218 let cols = [col("X", "fixed")];
219 assert_eq!(row_to_json(&row, &cols), json!({"X": null}));
220 }
221
222 #[test]
223 fn missing_trailing_cell_maps_to_null() {
224 let row: [Value; 0] = [];
225 let cols = [col("X", "text")];
226 assert_eq!(row_to_json(&row, &cols), json!({"X": null}));
227 }
228
229 #[test]
230 fn timestamp_passes_through_as_string() {
231 let row = [json!("1700000000.000000000")];
232 let cols = [col("TS", "timestamp_ntz")];
233 assert_eq!(
234 row_to_json(&row, &cols),
235 json!({"TS": "1700000000.000000000"})
236 );
237 }
238
239 #[test]
240 fn unknown_type_passes_through_as_string() {
241 let row = [json!("0xDEADBEEF")];
242 let cols = [col("BLOB", "binary")];
243 assert_eq!(row_to_json(&row, &cols), json!({"BLOB": "0xDEADBEEF"}));
244 }
245
246 #[test]
247 fn non_finite_real_falls_back_to_string() {
248 let row = [json!("NaN")];
249 let cols = [col("X", "real")];
250 assert_eq!(row_to_json(&row, &cols), json!({"X": "NaN"}));
252 }
253}