blob: 0cda73c60e4e7f7cf931ded83fb86b496358fd56 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! Verifies the correctness of the Arrow Spatial Bench generator by parsing the canonical TBL format
//! and comparing with the generated Arrow RecordBatches
use arrow::array::RecordBatch;
use arrow::datatypes::SchemaRef;
use spatialbench::csv::{BuildingCsv, CustomerCsv, DriverCsv, TripCsv, VehicleCsv};
use spatialbench::generators::{
Building, BuildingGenerator, Customer, CustomerGenerator, Driver, DriverGenerator, Trip,
TripGenerator, Vehicle, VehicleGenerator,
};
use spatialbench_arrow::{
BuildingArrow, CustomerArrow, DriverArrow, RecordBatchIterator, TripArrow, VehicleArrow,
};
use std::io::Write;
use std::sync::Arc;
use arrow::array::Array;
use arrow::array::BinaryArray;
use geo::Geometry;
use geozero::wkb::Wkb;
use geozero::ToGeo;
/// Macro that defines tests for tbl for a given type
macro_rules! test_row_type {
($FUNCNAME:ident, $GENERATOR:ty, $ARROWITER:ty, $FORMATTYPE:expr) => {
#[test]
fn $FUNCNAME() {
let scale_factor = 0.1;
let batch_size = 1000;
let part = 1;
let part_count = 1;
let generator = <$GENERATOR>::new(scale_factor, part, part_count);
$FORMATTYPE.test(
generator.clone().iter(),
<$ARROWITER>::new(generator).with_batch_size(batch_size),
);
}
};
}
test_row_type!(customer_tbl, CustomerGenerator, CustomerArrow, Test::tbl());
test_row_type!(customer_csv, CustomerGenerator, CustomerArrow, Test::csv());
test_row_type!(vehicle_tbl, VehicleGenerator, VehicleArrow, Test::tbl());
test_row_type!(vehicle_csv, VehicleGenerator, VehicleArrow, Test::csv());
test_row_type!(driver_tbl, DriverGenerator, DriverArrow, Test::tbl());
test_row_type!(driver_csv, DriverGenerator, DriverArrow, Test::csv());
test_row_type!(trip_tbl, TripGenerator, TripArrow, Test::tbl());
test_row_type!(trip_csv, TripGenerator, TripArrow, Test::csv());
test_row_type!(building_tbl, BuildingGenerator, BuildingArrow, Test::tbl());
test_row_type!(building_csv, BuildingGenerator, BuildingArrow, Test::csv());
/// Common trait for writing rows in TBL and CSV format
trait RowType {
/// write a row in TBL format, WITHOUT newline
fn write_tbl_row(self, text_data: &mut Vec<u8>);
/// write the header in CSV format
fn write_csv_header(text_data: &mut Vec<u8>);
/// write a row in CSV format, WITH newline
fn write_csv_row(self, text_data: &mut Vec<u8>);
}
/// Macro that implements the RowType trait for a given type
macro_rules! impl_row_type {
($type:ty, $csv_type:ty) => {
impl RowType for $type {
fn write_tbl_row(self, text_data: &mut Vec<u8>) {
write!(text_data, "{}", self).unwrap();
}
fn write_csv_header(text_data: &mut Vec<u8>) {
writeln!(text_data, "{}", <$csv_type>::header()).unwrap();
}
fn write_csv_row(self, text_data: &mut Vec<u8>) {
writeln!(text_data, "{}", <$csv_type>::new(self)).unwrap();
}
}
};
}
impl_row_type!(Customer<'_>, CustomerCsv);
impl_row_type!(Vehicle<'_>, VehicleCsv);
impl_row_type!(Driver, DriverCsv);
impl_row_type!(Trip, TripCsv);
impl_row_type!(Building<'_>, BuildingCsv);
#[derive(Debug, Clone, Copy)]
#[allow(clippy::upper_case_acronyms)]
enum Test {
/// Generate and parse data as TBL format ('|' delimited)
TBL,
/// Generate and parse data as CSV format
CSV,
}
impl Test {
fn tbl() -> Self {
Self::TBL
}
fn csv() -> Self {
Self::CSV
}
fn test<R, RI, RBI>(&self, mut row_iter: RI, mut arrow_iter: RBI)
where
R: RowType,
RI: Iterator<Item = R>,
RBI: RecordBatchIterator,
{
let schema = Arc::clone(arrow_iter.schema());
// Check for unsupported types for reparsing
let contains_binary = schema
.fields()
.iter()
.any(|f| matches!(f.data_type(), arrow::datatypes::DataType::Binary));
while let Some(arrow_batch) = arrow_iter.next() {
let batch_size = arrow_batch.num_rows();
for (i, field) in arrow_batch.schema().fields().iter().enumerate() {
if let arrow::datatypes::DataType::Binary = field.data_type() {
let col = arrow_batch.column(i);
let bin_array = col
.as_any()
.downcast_ref::<arrow::array::BinaryArray>()
.expect("Expected BinaryArray");
let expected_geoms = match field.name().as_str() {
"t_pickuploc" | "t_dropoffloc" => &["Point"][..],
"b_boundary" => &["Polygon"][..],
"z_boundary" => &["Polygon", "MultiPolygon"][..],
_ => &["Unknown"][..],
};
validate_wkb_column(bin_array, expected_geoms);
}
}
// Skip reparsing for Binary-containing schemas
if contains_binary {
continue;
}
let mut text_data = Vec::new();
self.write_header::<R>(&mut text_data);
row_iter.by_ref().take(batch_size).for_each(|item| {
self.write_row(item, &mut text_data);
});
let reparsed_batch = self.parse(&text_data, &schema, batch_size);
assert_eq!(reparsed_batch, arrow_batch);
}
}
fn write_header<R: RowType>(&self, text_data: &mut Vec<u8>) {
if let Test::CSV = self {
R::write_csv_header(text_data);
}
}
fn write_row<R: RowType>(&self, row: R, text_data: &mut Vec<u8>) {
match self {
Test::TBL => {
row.write_tbl_row(text_data);
let end_offset = text_data.len() - 1;
text_data[end_offset] = b'\n';
}
Test::CSV => {
row.write_csv_row(text_data);
}
}
}
fn parse(&self, data: &[u8], schema: &SchemaRef, batch_size: usize) -> RecordBatch {
let builder =
arrow_csv::reader::ReaderBuilder::new(Arc::clone(schema)).with_batch_size(batch_size);
let builder = match self {
Test::TBL => builder.with_header(false).with_delimiter(b'|'),
Test::CSV => builder.with_header(true),
};
let mut parser = builder.build(data).unwrap();
let batch = parser
.next()
.expect("should have a batch")
.expect("should have no errors parsing");
assert!(parser.next().is_none(), "should have only one batch");
batch
}
}
fn validate_wkb_column(array: &BinaryArray, expected_types: &[&str]) {
for i in 0..array.len() {
if array.is_null(i) {
panic!("Unexpected null geometry at row {i}");
}
let bytes = array.value(i);
let geom = Wkb(bytes.to_vec()).to_geo().unwrap_or_else(|err| {
panic!("Row {i}: Failed to parse WKB: {err}");
});
let type_name = match &geom {
Geometry::Point(_) => "Point",
Geometry::Line(_) => "Line",
Geometry::LineString(_) => "LineString",
Geometry::Polygon(_) => "Polygon",
Geometry::MultiPoint(_) => "MultiPoint",
Geometry::MultiLineString(_) => "MultiLineString",
Geometry::MultiPolygon(_) => "MultiPolygon",
Geometry::GeometryCollection(_) => "GeometryCollection",
_ => "Unknown", // Catch Rect, Triangle
};
assert!(
expected_types.contains(&type_name),
"Row {i}: Unexpected geometry type: got {}, expected one of {:?}",
type_name,
expected_types
);
}
}