use arrow::json::ArrayWriter;
use arrow::record_batch::RecordBatch;
use serde::Serialize;
use crate::engine::QueryOutput;
#[derive(Debug, thiserror::Error)]
pub(crate) enum OutputJsonError {
#[error("failed to encode result rows as JSON: {0}")]
Arrow(#[from] arrow::error::ArrowError),
#[error("failed to parse the encoded JSON rows: {0}")]
Parse(#[from] serde_json::Error),
}
#[derive(Debug, Clone, Serialize)]
pub struct QueryResultJson {
pub columns: Vec<String>,
pub rows: Vec<Vec<serde_json::Value>>,
pub truncated: bool,
pub skipped_partitions: usize,
}
pub(crate) fn output_to_json(
output: &QueryOutput,
skipped_partitions: usize,
) -> Result<QueryResultJson, OutputJsonError> {
let columns: Vec<String> = output
.schema
.fields()
.iter()
.map(|field| field.name().clone())
.collect();
if output.batches.is_empty() {
return Ok(QueryResultJson {
columns,
rows: Vec::new(),
truncated: output.truncated,
skipped_partitions,
});
}
let refs: Vec<&RecordBatch> = output.batches.iter().collect();
let mut buf = Vec::new();
{
let mut writer = ArrayWriter::new(&mut buf);
writer.write_batches(&refs)?;
writer.finish()?;
}
let parsed: Vec<serde_json::Value> = serde_json::from_slice(&buf)?;
let rows = parsed
.into_iter()
.map(|row| {
let mut obj = match row {
serde_json::Value::Object(map) => map,
_ => serde_json::Map::new(),
};
columns
.iter()
.map(|column| obj.remove(column).unwrap_or(serde_json::Value::Null))
.collect()
})
.collect();
Ok(QueryResultJson {
columns,
rows,
truncated: output.truncated,
skipped_partitions,
})
}
#[cfg(test)]
mod tests {
use arrow::array::{Int64Array, StringArray};
use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
use std::sync::Arc;
use super::*;
fn schema() -> SchemaRef {
Arc::new(Schema::new(vec![
Field::new("id", DataType::Int64, false),
Field::new("name", DataType::Utf8, false),
]))
}
fn batch(ids: &[i64], names: &[&str]) -> RecordBatch {
RecordBatch::try_new(
schema(),
vec![
Arc::new(Int64Array::from(ids.to_vec())),
Arc::new(StringArray::from(names.to_vec())),
],
)
.expect("build fixture batch")
}
#[test]
fn converts_columns_and_rows_in_schema_order() {
let output = QueryOutput {
batches: vec![batch(&[1, 2], &["a", "b"])],
truncated: false,
schema: schema(),
};
let json = output_to_json(&output, 0).expect("convert");
assert_eq!(json.columns, vec!["id", "name"]);
assert_eq!(
json.rows,
vec![
vec![serde_json::json!(1), serde_json::json!("a")],
vec![serde_json::json!(2), serde_json::json!("b")],
]
);
assert!(!json.truncated);
assert_eq!(json.skipped_partitions, 0);
}
#[test]
fn multiple_batches_concatenate_rows_in_order() {
let output = QueryOutput {
batches: vec![batch(&[1], &["a"]), batch(&[2], &["b"])],
truncated: true,
schema: schema(),
};
let json = output_to_json(&output, 0).expect("convert");
assert_eq!(json.rows.len(), 2);
assert_eq!(json.rows[0][0], serde_json::json!(1));
assert_eq!(json.rows[1][0], serde_json::json!(2));
assert!(json.truncated);
}
#[test]
fn zero_batches_still_yields_columns_from_the_planned_schema() {
let output = QueryOutput {
batches: Vec::new(),
truncated: false,
schema: schema(),
};
let json = output_to_json(&output, 0).expect("convert");
assert_eq!(json.columns, vec!["id", "name"]);
assert!(json.rows.is_empty());
assert_eq!(json.skipped_partitions, 0);
}
#[test]
fn skipped_partitions_is_stamped_from_the_caller_supplied_count() {
let with_rows = QueryOutput {
batches: vec![batch(&[1], &["a"])],
truncated: false,
schema: schema(),
};
let json = output_to_json(&with_rows, 3).expect("convert");
assert_eq!(json.skipped_partitions, 3);
let zero_batches = QueryOutput {
batches: Vec::new(),
truncated: false,
schema: schema(),
};
let json = output_to_json(&zero_batches, 3).expect("convert");
assert_eq!(
json.skipped_partitions, 3,
"the zero-batches early return must still stamp the caller's count"
);
}
}