use super::OrcOptions;
use crate::error::FaucetError;
use arrow::array::RecordBatch;
use arrow::datatypes::SchemaRef;
use orc_rust::projection::ProjectionMask;
use orc_rust::reader::ChunkReader;
use serde_json::Value;
#[derive(Debug)]
pub enum OrcInput {
File(std::fs::File),
Bytes(bytes::Bytes),
}
pub fn decode(bytes: &[u8], opts: &OrcOptions) -> Result<Vec<Value>, FaucetError> {
let mut out = Vec::new();
read_batches(
OrcInput::Bytes(bytes::Bytes::copy_from_slice(bytes)),
opts,
0,
&mut |b| {
out.extend(crate::columnar::record_batch_to_values(&b)?);
Ok(())
},
)?;
Ok(out)
}
pub fn read_batches(
input: OrcInput,
opts: &OrcOptions,
batch_size: usize,
f: &mut dyn FnMut(RecordBatch) -> Result<(), FaucetError>,
) -> Result<SchemaRef, FaucetError> {
read_batches_checked(input, opts, batch_size, &mut |_| Ok(()), f)
}
pub fn read_batches_checked(
input: OrcInput,
opts: &OrcOptions,
batch_size: usize,
check: &mut dyn FnMut(&SchemaRef) -> Result<(), FaucetError>,
f: &mut dyn FnMut(RecordBatch) -> Result<(), FaucetError>,
) -> Result<SchemaRef, FaucetError> {
match input {
OrcInput::File(file) => read_from(file, opts, batch_size, check, f),
OrcInput::Bytes(b) => read_from(b, opts, batch_size, check, f),
}
}
fn read_from<R: ChunkReader>(
reader: R,
opts: &OrcOptions,
batch_size: usize,
check: &mut dyn FnMut(&SchemaRef) -> Result<(), FaucetError>,
f: &mut dyn FnMut(RecordBatch) -> Result<(), FaucetError>,
) -> Result<SchemaRef, FaucetError> {
let mut builder = orc_rust::ArrowReaderBuilder::try_new(reader)
.map_err(|e| FaucetError::Source(format!("orc: not a readable ORC file: {e}")))?;
if let Some(cols) = &opts.columns {
let root = builder.file_metadata().root_data_type();
let known: Vec<&str> = root.children().iter().map(|c| c.name()).collect();
if let Some(missing) = cols.iter().find(|c| !known.contains(&c.as_str())) {
return Err(FaucetError::Source(format!(
"orc: column {missing:?} is not in the file (columns: {})",
known.join(", ")
)));
}
let mask = ProjectionMask::named_roots(root, cols);
builder = builder.with_projection(mask);
}
if batch_size > 0 {
builder = builder.with_batch_size(batch_size);
}
let schema = builder.schema();
check(&schema)?;
for batch in builder.build() {
let batch = batch.map_err(|e| FaucetError::Source(format!("orc: decode error: {e}")))?;
if batch.num_rows() > 0 {
f(batch)?;
}
}
Ok(schema)
}
#[cfg(test)]
mod tests {
use super::*;
const FIXTURE: &[u8] = include_bytes!("../../tests/fixtures/orc/people.orc");
#[test]
fn reads_every_column_and_row() {
let rows = decode(FIXTURE, &OrcOptions::default()).expect("decode");
assert_eq!(rows.len(), 3);
assert_eq!(rows[0]["id"], serde_json::json!(1));
assert_eq!(rows[0]["name"], serde_json::json!("ada"));
assert!(rows[0].get("score").is_some());
}
#[test]
fn projection_keeps_only_the_named_columns() {
let opts = OrcOptions {
columns: Some(vec!["name".into()]),
};
let mut schemas = Vec::new();
let mut rows = Vec::new();
let schema = read_batches(
OrcInput::Bytes(bytes::Bytes::from_static(FIXTURE)),
&opts,
2,
&mut |b| {
schemas.push(b.schema());
rows.extend(crate::columnar::record_batch_to_values(&b)?);
Ok(())
},
)
.expect("read");
assert_eq!(schema.fields().len(), 1);
assert_eq!(schemas.len(), 2, "batch size 2 over 3 rows");
assert_eq!(rows[2], serde_json::json!({"name": "grace"}));
}
#[test]
fn unknown_columns_and_garbage_are_errors() {
let opts = OrcOptions {
columns: Some(vec!["nope".into()]),
};
let err = decode(FIXTURE, &opts).expect_err("unknown column");
assert!(err.to_string().contains("nope"), "{err}");
let err = decode(b"definitely not orc", &OrcOptions::default()).expect_err("garbage");
assert!(err.to_string().contains("orc"), "{err}");
}
#[test]
fn reads_from_a_local_file() {
let dir = tempfile::tempdir().expect("tmp");
let path = dir.path().join("p.orc");
std::fs::write(&path, FIXTURE).expect("write");
let mut n = 0;
read_batches(
OrcInput::File(std::fs::File::open(&path).expect("open")),
&OrcOptions::default(),
0,
&mut |b| {
n += b.num_rows();
Ok(())
},
)
.expect("read");
assert_eq!(n, 3);
}
}