1use std::sync::Arc;
11
12use arrow_array::{
13 ArrayRef, RecordBatch,
14 builder::{BooleanBuilder, Float64Builder, Int64Builder, StringBuilder},
15};
16use arrow_schema::{DataType, Field, SchemaRef};
17use redis::Value;
18
19use crate::error::{FlightSqlError, Result};
20
21pub fn response_to_batch(response: Value, schema: &SchemaRef) -> Result<RecordBatch> {
26 let mut outer = match response {
27 Value::Array(v) => v,
28 other => {
29 return Err(FlightSqlError::MalformedResponse(format!(
30 "expected Array at top level, got {other:?}"
31 )));
32 }
33 };
34
35 if outer.len() < 2 {
36 return Ok(RecordBatch::new_empty(schema.clone()));
37 }
38
39 let rows_val = outer.remove(1);
40 let rows = match rows_val {
41 Value::Array(r) => r,
42 Value::Nil => vec![],
43 other => {
44 return Err(FlightSqlError::MalformedResponse(format!(
45 "expected Array for data rows, got {other:?}"
46 )));
47 }
48 };
49
50 let ncols = schema.fields().len();
51 let nrows = rows.len();
52
53 let mut builders: Vec<ColumnBuilder> = schema
54 .fields()
55 .iter()
56 .map(|f| ColumnBuilder::new(f.data_type(), nrows))
57 .collect();
58
59 for (row_idx, row_val) in rows.into_iter().enumerate() {
60 let cells = match row_val {
61 Value::Array(c) => c,
62 other => {
63 return Err(FlightSqlError::MalformedResponse(format!(
64 "row {row_idx}: expected Array, got {other:?}"
65 )));
66 }
67 };
68
69 if cells.len() != ncols {
70 return Err(FlightSqlError::ColumnCountMismatch {
71 schema: ncols,
72 row: cells.len(),
73 });
74 }
75
76 for (col_idx, cell) in cells.iter().enumerate() {
77 builders[col_idx].append(cell, row_idx)?;
78 }
79 }
80
81 let columns: Vec<ArrayRef> = builders.into_iter().map(|b| b.finish()).collect();
82 Ok(RecordBatch::try_new(schema.clone(), columns)?)
83}
84
85pub fn response_to_batch_auto(response: Value) -> Result<RecordBatch> {
103 let outer = match response {
104 Value::Array(v) => v,
105 other => {
106 return Err(FlightSqlError::MalformedResponse(format!(
107 "expected Array at top level, got {other:?}"
108 )));
109 }
110 };
111
112 if outer.len() < 2 {
113 return Ok(RecordBatch::new_empty(Arc::new(
114 arrow_schema::Schema::empty(),
115 )));
116 }
117
118 let col_names = extract_string_vec(&outer[0])?;
119 let rows: Vec<&Value> = match &outer[1] {
120 Value::Array(r) => r.iter().collect(),
121 Value::Nil => vec![],
122 other => {
123 return Err(FlightSqlError::MalformedResponse(format!(
124 "expected Array for data rows, got {other:?}"
125 )));
126 }
127 };
128
129 let ncols = col_names.len();
130 let nrows = rows.len();
131
132 if ncols == 0 {
133 return Ok(RecordBatch::new_empty(Arc::new(
134 arrow_schema::Schema::empty(),
135 )));
136 }
137
138 let col_types = sniff_column_types(&rows, ncols);
141
142 let schema = Arc::new(arrow_schema::Schema::new(
143 col_names
144 .iter()
145 .zip(&col_types)
146 .map(|(n, dt)| Field::new(n.as_str(), dt.clone(), true))
147 .collect::<Vec<_>>(),
148 ));
149
150 let mut builders: Vec<ColumnBuilder> = col_types
152 .iter()
153 .map(|dt| ColumnBuilder::new(dt, nrows))
154 .collect();
155
156 for (row_idx, row_val) in rows.iter().enumerate() {
157 let cells = match row_val {
158 Value::Array(c) => c,
159 other => {
160 return Err(FlightSqlError::MalformedResponse(format!(
161 "row {row_idx}: expected Array, got {other:?}"
162 )));
163 }
164 };
165 if cells.len() != ncols {
171 return Err(FlightSqlError::ColumnCountMismatch {
172 schema: ncols,
173 row: cells.len(),
174 });
175 }
176
177 for (col_idx, cell) in cells.iter().enumerate() {
178 builders[col_idx].append(cell, row_idx)?;
179 }
180 }
181
182 let columns: Vec<ArrayRef> = builders.into_iter().map(|b| b.finish()).collect();
183 Ok(RecordBatch::try_new(schema, columns)?)
184}
185
186#[inline]
192fn cell_type(cell: &Value) -> Option<DataType> {
193 match cell {
194 Value::Nil => None, Value::Int(_) => Some(DataType::Int64),
196 Value::Double(_) => Some(DataType::Float64),
197 Value::Boolean(_) => Some(DataType::Boolean),
198 _ => Some(DataType::Utf8),
199 }
200}
201
202fn sniff_column_types(rows: &[&Value], ncols: usize) -> Vec<DataType> {
231 let mut types = vec![DataType::Utf8; ncols];
233 let mut determined = vec![false; ncols];
235 let mut remaining = ncols;
236
237 for row in rows {
238 if remaining == 0 {
239 break; }
241 let Value::Array(cells) = row else { continue };
242 for ci in 0..ncols {
243 if determined[ci] {
244 continue; }
246 if let Some(cell) = cells.get(ci)
247 && let Some(dt) = cell_type(cell)
248 {
249 types[ci] = dt;
250 determined[ci] = true;
251 remaining -= 1;
252 }
253 }
254 }
255 types
256}
257
258enum ColumnBuilder {
261 Int64(Int64Builder),
262 Float64(Float64Builder),
263 Bool(BooleanBuilder),
264 Utf8(StringBuilder),
265}
266
267impl ColumnBuilder {
268 fn new(dt: &DataType, capacity: usize) -> Self {
269 match dt {
270 DataType::Int8
271 | DataType::Int16
272 | DataType::Int32
273 | DataType::Int64
274 | DataType::UInt8
275 | DataType::UInt16
276 | DataType::UInt32
277 | DataType::UInt64 => Self::Int64(Int64Builder::with_capacity(capacity)),
278 DataType::Float16 | DataType::Float32 | DataType::Float64 => {
279 Self::Float64(Float64Builder::with_capacity(capacity))
280 }
281 DataType::Boolean => Self::Bool(BooleanBuilder::with_capacity(capacity)),
282 _ => Self::Utf8(StringBuilder::with_capacity(capacity, capacity * 16)),
283 }
284 }
285
286 fn append(&mut self, val: &Value, row: usize) -> Result<()> {
287 match self {
288 Self::Int64(b) => b.append_option(redis_to_i64(val, row)?),
289 Self::Float64(b) => b.append_option(redis_to_f64(val, row)?),
290 Self::Bool(b) => b.append_option(redis_to_bool(val, row)?),
291 Self::Utf8(b) => append_redis_str(b, val),
294 }
295 Ok(())
296 }
297
298 fn finish(self) -> ArrayRef {
299 match self {
300 Self::Int64(mut b) => Arc::new(b.finish()),
301 Self::Float64(mut b) => Arc::new(b.finish()),
302 Self::Bool(mut b) => Arc::new(b.finish()),
303 Self::Utf8(mut b) => Arc::new(b.finish()),
304 }
305 }
306}
307
308fn redis_to_i64(v: &Value, row: usize) -> Result<Option<i64>> {
311 match v {
312 Value::Nil => Ok(None),
313 Value::Int(i) => Ok(Some(*i)),
314 Value::Double(d) => Ok(Some(*d as i64)),
315 Value::BulkString(b) => {
316 let s = std::str::from_utf8(b).unwrap_or("");
317 s.parse::<i64>()
318 .map(Some)
319 .map_err(|_| malformed(row, "expected i64", s))
320 }
321 Value::SimpleString(s) => s
322 .parse::<i64>()
323 .map(Some)
324 .map_err(|_| malformed(row, "expected i64", s)),
325 other => Err(malformed(
326 row,
327 "expected Int for Int64",
328 &format!("{other:?}"),
329 )),
330 }
331}
332
333fn redis_to_f64(v: &Value, row: usize) -> Result<Option<f64>> {
334 match v {
335 Value::Nil => Ok(None),
336 Value::Double(d) => Ok(Some(*d)),
337 Value::Int(i) => Ok(Some(*i as f64)),
338 Value::BulkString(b) => {
339 let s = std::str::from_utf8(b).unwrap_or("");
340 s.parse::<f64>()
341 .map(Some)
342 .map_err(|_| malformed(row, "expected f64", s))
343 }
344 Value::SimpleString(s) => s
345 .parse::<f64>()
346 .map(Some)
347 .map_err(|_| malformed(row, "expected f64", s)),
348 other => Err(malformed(
349 row,
350 "expected Double for Float64",
351 &format!("{other:?}"),
352 )),
353 }
354}
355
356fn redis_to_bool(v: &Value, row: usize) -> Result<Option<bool>> {
357 match v {
358 Value::Nil => Ok(None),
359 Value::Boolean(b) => Ok(Some(*b)),
360 Value::Int(i) => Ok(Some(*i != 0)),
361 Value::BulkString(b) => match b.as_slice() {
362 b"true" | b"1" => Ok(Some(true)),
363 b"false" | b"0" => Ok(Some(false)),
364 other => Err(malformed(
365 row,
366 "expected bool",
367 std::str::from_utf8(other).unwrap_or("?"),
368 )),
369 },
370 other => Err(malformed(row, "expected Boolean", &format!("{other:?}"))),
371 }
372}
373
374fn append_redis_str(b: &mut StringBuilder, v: &Value) {
378 match v {
379 Value::Nil => b.append_null(),
380 Value::BulkString(bytes) => b.append_value(String::from_utf8_lossy(bytes)),
381 Value::SimpleString(s) => b.append_value(s),
382 Value::Int(i) => b.append_value(i.to_string()),
383 Value::Double(d) => b.append_value(d.to_string()),
384 Value::Boolean(x) => b.append_value(if *x { "true" } else { "false" }),
385 other => b.append_value(format!("{other:?}")),
386 }
387}
388
389fn extract_string_vec(v: &Value) -> Result<Vec<String>> {
390 match v {
391 Value::Array(items) => items
392 .iter()
393 .map(|item| match item {
394 Value::BulkString(b) => Ok(String::from_utf8_lossy(b).into_owned()),
395 Value::SimpleString(s) => Ok(s.clone()),
396 other => Ok(format!("{other:?}")),
397 })
398 .collect(),
399 other => Err(FlightSqlError::MalformedResponse(format!(
400 "expected Array for column names, got {other:?}"
401 ))),
402 }
403}
404
405fn malformed(row: usize, expected: &str, got: &str) -> FlightSqlError {
406 FlightSqlError::MalformedResponse(format!("row {row}: {expected}, got '{got}'"))
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 fn header(names: &[&str]) -> Value {
414 Value::Array(names.iter().map(|n| Value::BulkString(n.as_bytes().to_vec())).collect())
415 }
416
417 fn row(cells: Vec<Value>) -> Value {
418 Value::Array(cells)
419 }
420
421 #[test]
423 fn auto_well_formed_rows_build_batch() {
424 let resp = Value::Array(vec![
425 header(&["id", "name"]),
426 Value::Array(vec![
427 row(vec![Value::Int(1), Value::BulkString(b"a".to_vec())]),
428 row(vec![Value::Int(2), Value::BulkString(b"b".to_vec())]),
429 ]),
430 ]);
431 let batch = response_to_batch_auto(resp).unwrap();
432 assert_eq!(batch.num_rows(), 2);
433 assert_eq!(batch.num_columns(), 2);
434 }
435
436 #[test]
440 fn auto_short_row_is_column_count_mismatch() {
441 let resp = Value::Array(vec![
442 header(&["id", "name"]),
443 Value::Array(vec![
444 row(vec![Value::Int(1), Value::BulkString(b"a".to_vec())]),
445 row(vec![Value::Int(2)]), ]),
447 ]);
448 let err = response_to_batch_auto(resp).unwrap_err();
449 match err {
450 FlightSqlError::ColumnCountMismatch { schema, row } => {
451 assert_eq!(schema, 2);
452 assert_eq!(row, 1);
453 }
454 other => panic!("expected ColumnCountMismatch, got {other:?}"),
455 }
456 }
457
458 #[test]
461 fn auto_long_row_is_column_count_mismatch() {
462 let resp = Value::Array(vec![
463 header(&["id"]),
464 Value::Array(vec![row(vec![Value::Int(1), Value::Int(99)])]),
465 ]);
466 let err = response_to_batch_auto(resp).unwrap_err();
467 assert!(matches!(
468 err,
469 FlightSqlError::ColumnCountMismatch { schema: 1, row: 2 }
470 ));
471 }
472
473 #[test]
480 fn auto_nested_cypher_value_falls_back_to_utf8() {
481 let cypher_map = Value::Map(vec![
483 (
484 Value::BulkString(b"name".to_vec()),
485 Value::BulkString(b"alice".to_vec()),
486 ),
487 (Value::BulkString(b"age".to_vec()), Value::Int(30)),
488 ]);
489 let cypher_list = Value::Array(vec![Value::Int(1), Value::Int(2), Value::Int(3)]);
491
492 let resp = Value::Array(vec![
493 header(&["id", "props", "tags"]),
494 Value::Array(vec![row(vec![
495 Value::Int(7),
496 cypher_map.clone(),
497 cypher_list.clone(),
498 ])]),
499 ]);
500 let batch = response_to_batch_auto(resp).unwrap();
501 assert_eq!(batch.num_rows(), 1);
502 assert_eq!(batch.num_columns(), 3);
503
504 assert_eq!(batch.schema().field(0).data_type(), &DataType::Int64);
506 assert_eq!(batch.schema().field(1).data_type(), &DataType::Utf8);
507 assert_eq!(batch.schema().field(2).data_type(), &DataType::Utf8);
508
509 let props = batch
512 .column(1)
513 .as_any()
514 .downcast_ref::<arrow_array::StringArray>()
515 .unwrap();
516 assert_eq!(props.value(0), format!("{cypher_map:?}"));
517 let tags = batch
518 .column(2)
519 .as_any()
520 .downcast_ref::<arrow_array::StringArray>()
521 .unwrap();
522 assert_eq!(tags.value(0), format!("{cypher_list:?}"));
523 }
524
525 #[test]
528 fn cell_type_maps_nested_values_to_utf8() {
529 assert_eq!(cell_type(&Value::Int(1)), Some(DataType::Int64));
530 assert_eq!(cell_type(&Value::Double(1.0)), Some(DataType::Float64));
531 assert_eq!(cell_type(&Value::Boolean(true)), Some(DataType::Boolean));
532 assert_eq!(cell_type(&Value::Nil), None);
533 assert_eq!(cell_type(&Value::Map(vec![])), Some(DataType::Utf8));
535 assert_eq!(cell_type(&Value::Array(vec![])), Some(DataType::Utf8));
536 assert_eq!(
537 cell_type(&Value::BulkString(b"x".to_vec())),
538 Some(DataType::Utf8)
539 );
540 }
541}