use crate::FaucetError;
use arrow::array::RecordBatch;
use arrow::datatypes::{Schema, SchemaRef};
use serde_json::Value;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct ColumnarPage {
pub batch: RecordBatch,
pub bookmark: Option<Value>,
}
impl ColumnarPage {
pub fn new(batch: RecordBatch, bookmark: Option<Value>) -> Self {
Self { batch, bookmark }
}
pub fn num_rows(&self) -> usize {
self.batch.num_rows()
}
}
fn te<E: std::fmt::Display>(ctx: &str, e: E) -> FaucetError {
FaucetError::Transform(format!("columnar shim: {ctx}: {e}"))
}
pub fn infer_arrow_schema(records: &[Value]) -> Result<SchemaRef, FaucetError> {
let iter = records
.iter()
.map(|v| Ok::<_, arrow::error::ArrowError>(v.clone()));
let schema = arrow_json::reader::infer_json_schema_from_iterator(iter)
.map_err(|e| te("schema inference", e))?;
Ok(Arc::new(schema))
}
pub fn values_to_record_batch(
records: &[Value],
schema: SchemaRef,
) -> Result<RecordBatch, FaucetError> {
let mut decoder = arrow_json::ReaderBuilder::new(schema.clone())
.build_decoder()
.map_err(|e| te("decoder build", e))?;
decoder.serialize(records).map_err(|e| te("encode", e))?;
let mut batches = Vec::new();
while let Some(b) = decoder.flush().map_err(|e| te("flush", e))? {
batches.push(b);
}
if batches.is_empty() {
return Ok(RecordBatch::new_empty(schema));
}
if batches.len() == 1 {
return Ok(batches.pop().unwrap());
}
arrow::compute::concat_batches(&schema, &batches).map_err(|e| te("concat", e))
}
pub fn values_to_record_batch_inferred(records: &[Value]) -> Result<RecordBatch, FaucetError> {
let schema = infer_arrow_schema(records)?;
values_to_record_batch(records, schema)
}
pub fn record_batch_to_values(batch: &RecordBatch) -> Result<Vec<Value>, FaucetError> {
let mut buf = Vec::new();
{
let mut writer = arrow_json::writer::WriterBuilder::new()
.with_explicit_nulls(true)
.build::<_, arrow_json::writer::JsonArray>(&mut buf);
writer.write(batch).map_err(|e| te("json write", e))?;
writer.finish().map_err(|e| te("json finish", e))?;
}
serde_json::from_slice(&buf).map_err(|e| te("json parse", e))
}
pub fn schema_eq(a: &Schema, b: &Schema) -> bool {
a.fields() == b.fields()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn round_trip_scalars_nulls_nested() {
let recs = vec![
json!({"id": 1, "name": "a", "score": 1.5, "ok": true, "tags": ["x", "y"]}),
json!({"id": 2, "name": null, "score": null, "ok": false, "tags": []}),
];
let batch = values_to_record_batch_inferred(&recs).unwrap();
assert_eq!(batch.num_rows(), 2);
let back = record_batch_to_values(&batch).unwrap();
assert_eq!(back[0]["id"], json!(1));
assert_eq!(back[0]["tags"], json!(["x", "y"]));
assert!(back[1].as_object().unwrap().contains_key("name"));
assert_eq!(back[1]["name"], json!(null));
}
#[test]
fn empty_records_yield_empty_batch_and_back() {
let schema = infer_arrow_schema(&[json!({"a": 1})]).unwrap();
let batch = values_to_record_batch(&[], schema).unwrap();
assert_eq!(batch.num_rows(), 0);
assert!(record_batch_to_values(&batch).unwrap().is_empty());
}
#[test]
fn columnar_page_reports_rows() {
let batch = values_to_record_batch_inferred(&[json!({"a": 1}), json!({"a": 2})]).unwrap();
let page = ColumnarPage::new(batch, Some(json!({"lsn": 42})));
assert_eq!(page.num_rows(), 2);
assert_eq!(page.bookmark, Some(json!({"lsn": 42})));
}
}