1use std::collections::HashMap;
10use std::sync::Arc;
11
12use chrono::{DateTime, Days, NaiveDate, NaiveTime, Utc};
13use serde_json::{json, Map, Value};
14
15#[derive(Clone, Debug, PartialEq, Eq)]
17pub struct Column {
18 pub name: String,
20 pub ty: String,
22 pub nullable: bool,
24 pub length: Option<i64>,
26 pub precision: Option<i64>,
28 pub scale: Option<i64>,
30}
31
32impl Column {
33 #[must_use]
36 pub fn type_label(&self) -> String {
37 match (self.precision, self.scale, self.length) {
38 (Some(p), Some(s), _) => format!("{}({p},{s})", self.ty),
39 (Some(p), None, _) => format!("{}({p})", self.ty),
40 (None, Some(s), _) => format!("{}({s})", self.ty),
41 (None, None, Some(l)) => format!("{}({l})", self.ty),
42 _ => self.ty.clone(),
43 }
44 }
45}
46
47#[derive(Clone, Debug)]
49pub struct Row {
50 values: Vec<Option<String>>,
51 columns: Arc<Vec<Column>>,
52 index: Arc<HashMap<String, usize>>,
53}
54
55impl Row {
56 #[must_use]
58 pub fn new(
59 values: Vec<Option<String>>,
60 columns: Arc<Vec<Column>>,
61 index: Arc<HashMap<String, usize>>,
62 ) -> Self {
63 Self {
64 values,
65 columns,
66 index,
67 }
68 }
69
70 #[must_use]
72 pub fn columns(&self) -> &[Column] {
73 &self.columns
74 }
75
76 #[must_use]
78 pub fn raw_at(&self, index: usize) -> Option<&str> {
79 self.values.get(index).and_then(|v| v.as_deref())
80 }
81
82 #[must_use]
84 pub fn raw(&self, name: &str) -> Option<&str> {
85 self.index
86 .get(&name.to_ascii_uppercase())
87 .and_then(|&i| self.raw_at(i))
88 }
89
90 #[must_use]
96 pub fn to_json_object(&self) -> Map<String, Value> {
97 let mut map = Map::with_capacity(self.columns.len());
98 let mut seen: HashMap<&str, u32> = HashMap::new();
99 for (i, col) in self.columns.iter().enumerate() {
100 let raw = self.values.get(i).and_then(|v| v.as_deref());
101 let value = value_to_json(raw, col);
102 let count = seen.entry(col.name.as_str()).or_insert(0);
103 *count += 1;
104 let key = if *count == 1 {
105 col.name.clone()
106 } else {
107 format!("{}_{}", col.name, *count)
108 };
109 map.insert(key, value);
110 }
111 map
112 }
113}
114
115#[must_use]
119pub fn rows_to_payload(rows: &[Row]) -> Value {
120 let columns: Vec<Value> = rows.first().map_or_else(Vec::new, |row| {
121 row.columns()
122 .iter()
123 .map(|c| json!({ "name": c.name, "type": c.type_label() }))
124 .collect()
125 });
126 let out: Vec<Value> = rows
127 .iter()
128 .map(|r| Value::Object(r.to_json_object()))
129 .collect();
130 json!({ "columns": columns, "rows": out })
131}
132
133#[must_use]
138pub fn rows_to_multi_payload(statements: &[Vec<Row>]) -> Value {
139 let statements: Vec<Value> = statements
140 .iter()
141 .map(|rows| rows_to_payload(rows))
142 .collect();
143 json!({ "statements": statements })
144}
145
146#[must_use]
149pub fn value_to_json(raw: Option<&str>, col: &Column) -> Value {
150 let Some(s) = raw else { return Value::Null };
151 match col.ty.as_str() {
152 "fixed" if col.scale.unwrap_or(0) == 0 => s
153 .parse::<i64>()
154 .map_or_else(|_| Value::String(s.to_string()), |n| json!(n)),
155 "fixed" | "real" | "float" | "double" | "double precision" => number_or_string(s),
156 "boolean" => match s {
157 "1" => json!(true),
158 "0" => json!(false),
159 _ if s.eq_ignore_ascii_case("true") => json!(true),
160 _ if s.eq_ignore_ascii_case("false") => json!(false),
161 _ => Value::String(s.to_string()),
162 },
163 "date" => decode_date(s),
164 "time" => decode_time(s),
165 "timestamp_ntz" => decode_epoch(s).map_or_else(
166 || Value::String(s.to_string()),
167 |dt| json!(dt.naive_utc().format("%Y-%m-%dT%H:%M:%S%.f").to_string()),
168 ),
169 "timestamp_ltz" => decode_epoch(s)
170 .map_or_else(|| Value::String(s.to_string()), |dt| json!(dt.to_rfc3339())),
171 "timestamp_tz" => decode_timestamp_tz(s),
172 "variant" | "object" | "array" => {
173 serde_json::from_str(s).unwrap_or_else(|_| Value::String(s.to_string()))
174 }
175 _ => Value::String(s.to_string()),
178 }
179}
180
181fn number_or_string(s: &str) -> Value {
184 s.parse::<f64>()
185 .ok()
186 .and_then(serde_json::Number::from_f64)
187 .map_or_else(|| Value::String(s.to_string()), Value::Number)
188}
189
190fn decode_date(s: &str) -> Value {
192 let Ok(days) = s.parse::<i64>() else {
193 return Value::String(s.to_string());
194 };
195 let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap_or_default();
196 let date = if days >= 0 {
197 epoch.checked_add_days(Days::new(days.unsigned_abs()))
198 } else {
199 epoch.checked_sub_days(Days::new(days.unsigned_abs()))
200 };
201 date.map_or_else(
202 || Value::String(s.to_string()),
203 |d| json!(d.format("%Y-%m-%d").to_string()),
204 )
205}
206
207fn decode_time(s: &str) -> Value {
209 let Some((secs, nanos)) = split_seconds(s) else {
210 return Value::String(s.to_string());
211 };
212 if secs < 0 {
213 return Value::String(s.to_string());
214 }
215 NaiveTime::from_num_seconds_from_midnight_opt(secs.unsigned_abs() as u32, nanos).map_or_else(
216 || Value::String(s.to_string()),
217 |t| json!(t.format("%H:%M:%S%.f").to_string()),
218 )
219}
220
221fn decode_timestamp_tz(s: &str) -> Value {
224 let epoch = s.split_whitespace().next().unwrap_or(s);
225 decode_epoch(epoch).map_or_else(|| Value::String(s.to_string()), |dt| json!(dt.to_rfc3339()))
226}
227
228fn decode_epoch(s: &str) -> Option<DateTime<Utc>> {
230 let (secs, nanos) = split_seconds(s)?;
231 DateTime::from_timestamp(secs, nanos)
232}
233
234fn split_seconds(s: &str) -> Option<(i64, u32)> {
237 let s = s.trim();
238 let negative = s.starts_with('-');
239 let (secs_str, frac_str) = s.split_once('.').unwrap_or((s, ""));
240 let mut secs = secs_str.parse::<i64>().ok()?;
241 if frac_str.is_empty() {
242 return Some((secs, 0));
243 }
244 if !frac_str.bytes().all(|b| b.is_ascii_digit()) {
245 return None;
246 }
247 let mut digits: Vec<u8> = frac_str.bytes().take(9).collect();
249 digits.resize(9, b'0');
250 let mut nanos = std::str::from_utf8(&digits).ok()?.parse::<u32>().ok()?;
251 if negative && nanos > 0 {
252 secs -= 1;
254 nanos = 1_000_000_000 - nanos;
255 }
256 Some((secs, nanos))
257}
258
259#[cfg(test)]
260#[allow(clippy::unwrap_used, clippy::expect_used)]
261mod tests {
262 use super::*;
263
264 fn col(ty: &str, precision: Option<i64>, scale: Option<i64>, length: Option<i64>) -> Column {
265 Column {
266 name: "C".to_string(),
267 ty: ty.to_string(),
268 nullable: true,
269 length,
270 precision,
271 scale,
272 }
273 }
274
275 #[test]
276 fn type_label_renders_parameters() {
277 assert_eq!(
278 col("fixed", Some(10), Some(2), None).type_label(),
279 "fixed(10,2)"
280 );
281 assert_eq!(col("text", None, None, Some(255)).type_label(), "text(255)");
282 assert_eq!(
283 col("timestamp_ntz", None, None, None).type_label(),
284 "timestamp_ntz"
285 );
286 }
287
288 #[test]
289 fn null_is_json_null() {
290 assert_eq!(
291 value_to_json(None, &col("fixed", None, Some(0), None)),
292 Value::Null
293 );
294 }
295
296 #[test]
297 fn decodes_each_type() {
298 assert_eq!(
299 value_to_json(Some("42"), &col("fixed", Some(38), Some(0), None)),
300 json!(42)
301 );
302 let big = "123456789012345678901234567890";
303 assert_eq!(
304 value_to_json(Some(big), &col("fixed", Some(38), Some(0), None)),
305 json!(big),
306 "overflowing integer keeps the exact string"
307 );
308 assert_eq!(
309 value_to_json(Some("123.45"), &col("fixed", Some(10), Some(2), None)),
310 json!(123.45)
311 );
312 assert_eq!(
313 value_to_json(Some("1.5"), &col("real", None, None, None)),
314 json!(1.5)
315 );
316 assert_eq!(
317 value_to_json(Some("1"), &col("boolean", None, None, None)),
318 json!(true)
319 );
320 assert_eq!(
321 value_to_json(Some("0"), &col("boolean", None, None, None)),
322 json!(false)
323 );
324 assert_eq!(
325 value_to_json(Some("hello"), &col("text", None, None, Some(255))),
326 json!("hello")
327 );
328 assert_eq!(
329 value_to_json(Some("0"), &col("date", None, None, None)),
330 json!("1970-01-01")
331 );
332 assert_eq!(
333 value_to_json(Some("19723"), &col("date", None, None, None)),
334 json!("2024-01-01")
335 );
336 assert_eq!(
337 value_to_json(Some("3661"), &col("time", None, None, Some(0))),
338 json!("01:01:01")
339 );
340 assert_eq!(
341 value_to_json(
342 Some("1704067200"),
343 &col("timestamp_ntz", None, None, Some(0))
344 ),
345 json!("2024-01-01T00:00:00")
346 );
347 assert_eq!(
348 value_to_json(
349 Some("1704067200"),
350 &col("timestamp_ltz", None, None, Some(0))
351 ),
352 json!("2024-01-01T00:00:00+00:00")
353 );
354 assert_eq!(
355 value_to_json(
356 Some("1704067200.000 1440"),
357 &col("timestamp_tz", None, None, Some(3))
358 ),
359 json!("2024-01-01T00:00:00+00:00")
360 );
361 assert_eq!(
362 value_to_json(Some(r#"{"a":1}"#), &col("variant", None, None, None)),
363 json!({ "a": 1 })
364 );
365 assert_eq!(
366 value_to_json(Some("DEADBEEF"), &col("binary", None, None, None)),
367 json!("DEADBEEF")
368 );
369 }
370
371 #[test]
372 fn malformed_value_falls_back_to_raw() {
373 assert_eq!(
374 value_to_json(Some("maybe"), &col("boolean", None, None, None)),
375 json!("maybe")
376 );
377 }
378
379 #[test]
380 fn row_to_json_keys_by_column_name() {
381 let columns = Arc::new(vec![
382 Column {
383 name: "ID".to_string(),
384 ty: "fixed".to_string(),
385 nullable: false,
386 length: None,
387 precision: Some(38),
388 scale: Some(0),
389 },
390 Column {
391 name: "NAME".to_string(),
392 ty: "text".to_string(),
393 nullable: true,
394 length: None,
395 precision: None,
396 scale: None,
397 },
398 ]);
399 let mut index = HashMap::new();
400 index.insert("ID".to_string(), 0);
401 index.insert("NAME".to_string(), 1);
402 let row = Row::new(
403 vec![Some("1".to_string()), Some("hi".to_string())],
404 columns,
405 Arc::new(index),
406 );
407 let obj = row.to_json_object();
408 assert_eq!(obj.get("ID"), Some(&json!(1)));
409 assert_eq!(obj.get("NAME"), Some(&json!("hi")));
410 assert_eq!(obj.len(), 2);
411 }
412
413 #[test]
414 fn duplicate_column_names_are_disambiguated() {
415 let dup = || Column {
416 name: "N".to_string(),
417 ty: "fixed".to_string(),
418 nullable: false,
419 length: None,
420 precision: Some(38),
421 scale: Some(0),
422 };
423 let columns = Arc::new(vec![dup(), dup()]);
424 let mut index = HashMap::new();
425 index.insert("N".to_string(), 0);
426 let row = Row::new(
427 vec![Some("1".to_string()), Some("2".to_string())],
428 columns,
429 Arc::new(index),
430 );
431 let obj = row.to_json_object();
432 assert_eq!(obj.get("N"), Some(&json!(1)));
433 assert_eq!(obj.get("N_2"), Some(&json!(2)), "second N is not dropped");
434 assert_eq!(obj.len(), 2);
435 }
436
437 #[test]
438 fn rows_to_multi_payload_wraps_each_statement() {
439 let columns = Arc::new(vec![Column {
440 name: "N".to_string(),
441 ty: "fixed".to_string(),
442 nullable: false,
443 length: None,
444 precision: Some(38),
445 scale: Some(0),
446 }]);
447 let index = Arc::new(HashMap::from([("N".to_string(), 0)]));
448 let row = Row::new(vec![Some("1".to_string())], columns, index);
449
450 let one = rows_to_multi_payload(&[vec![row.clone()]]);
452 assert_eq!(one["statements"].as_array().unwrap().len(), 1);
453 assert_eq!(one["statements"][0]["rows"][0]["N"], json!(1));
454
455 let two = rows_to_multi_payload(&[vec![row], vec![]]);
457 let stmts = two["statements"].as_array().unwrap();
458 assert_eq!(stmts.len(), 2);
459 assert_eq!(stmts[1], json!({ "columns": [], "rows": [] }));
460 }
461}