use std::time::Instant;
use geo_types::{Geometry, LineString, Polygon};
use geopackage::arrow::ArrowReadOptions;
use geopackage::core::datetime::DateTime;
use geopackage::core::types::{ColumnType, GeometryType};
use geopackage::{ColumnSpec, GeoPackage, GeometrySpec, NewFeature, TableSchemaBuilder, Value};
fn coord(i: usize) -> (f64, f64) {
let f = i as f64;
let x = (f * 0.618_033_988_75).rem_euclid(360.0) - 180.0;
let y = (f * 0.314_159_265_36).rem_euclid(180.0) - 90.0;
(x, y)
}
fn category(i: usize) -> String {
["alpha", "beta", "gamma", "delta"]
.get(i % 4)
.copied()
.unwrap_or("alpha")
.to_owned()
}
fn footprint(x: f64, y: f64) -> Geometry<f64> {
const VERTICES: usize = 10;
let mut points: Vec<(f64, f64)> = (0..VERTICES)
.map(|v| {
let angle = (v as f64) * std::f64::consts::TAU / (VERTICES as f64);
let radius = 0.000_5 + 0.000_2 * ((v % 3) as f64);
(x + radius * angle.cos(), y + radius * angle.sin())
})
.collect();
if let Some(&first) = points.first() {
points.push(first);
}
Geometry::Polygon(Polygon::new(LineString::from(points), Vec::new()))
}
fn fixture(path: &str, rows: usize) -> Result<(), Box<dyn std::error::Error>> {
let start = Instant::now();
let gpkg = GeoPackage::create(path)?;
let builder = TableSchemaBuilder::new("features")
.column(ColumnSpec::new("name", ColumnType::Text(None)))
.column(ColumnSpec::new("category", ColumnType::Text(None)))
.column(ColumnSpec::new("source", ColumnType::Text(None)))
.column(ColumnSpec::new("quality", ColumnType::Text(None)))
.column(ColumnSpec::new("count", ColumnType::Integer))
.column(ColumnSpec::new("code", ColumnType::MediumInt))
.column(ColumnSpec::new("floors", ColumnType::SmallInt))
.column(ColumnSpec::new("built_year", ColumnType::SmallInt))
.column(ColumnSpec::new("area", ColumnType::Double))
.column(ColumnSpec::new("height", ColumnType::Float))
.column(ColumnSpec::new("active", ColumnType::Boolean))
.column(ColumnSpec::new("surveyed", ColumnType::DateTime))
.column(ColumnSpec::new("payload", ColumnType::Blob(None)))
.geometry(GeometrySpec::new(GeometryType::Polygon, 4326))
.spatial_index(false);
let layer = gpkg.create_layer(&builder)?;
let stamp = DateTime::parse_strict("2026-07-24T12:34:56.789Z")?;
let features: Vec<NewFeature<Geometry<f64>>> = (0..rows)
.map(|i| {
let (x, y) = coord(i);
let f = i as f64;
NewFeature::new(
footprint(x, y),
vec![
Value::Text(format!("building {i}")),
Value::Text(category(i)),
Value::Text("survey".to_owned()),
Value::Text(category(i + 1)),
Value::Integer(i as i64),
Value::Integer((i % 30_000) as i64),
Value::Integer((i % 40) as i64),
Value::Integer(1900 + (i % 125) as i64),
Value::Float(f * 1.5),
Value::Float(f * 0.25),
Value::Boolean(i % 2 == 0),
Value::DateTime(stamp),
Value::Blob(vec![0xde, 0xad, 0xbe, 0xef]),
],
)
})
.collect();
layer.write_all(features, 0)?;
gpkg.close()?;
println!("elapsed_ms={:.3}", start.elapsed().as_secs_f64() * 1000.0);
println!("rows={rows}");
Ok(())
}
fn noop(path: &str) -> Result<(), Box<dyn std::error::Error>> {
let start = Instant::now();
let gpkg = GeoPackage::open(path)?;
drop(gpkg);
println!("elapsed_ms={:.3}", start.elapsed().as_secs_f64() * 1000.0);
Ok(())
}
fn read(path: &str, reps: usize, threads: usize) -> Result<(), Box<dyn std::error::Error>> {
let gpkg = GeoPackage::open(path)?;
let layer = gpkg.layer("features")?;
let options = ArrowReadOptions::default().with_threads(threads);
let start = Instant::now();
for _ in 1..reps {
let batches = layer.read_arrow(options)?;
for batch in batches {
std::hint::black_box(batch?.num_rows());
}
}
let batches = layer.read_arrow(options)?;
let mut rows = 0usize;
let mut count = 0usize;
let mut columns = 0usize;
for batch in batches {
let batch = batch?;
rows += batch.num_rows();
columns = batch.num_columns();
count += 1;
}
let elapsed = start.elapsed();
println!(
"elapsed_ms={:.3}",
elapsed.as_secs_f64() * 1000.0 / reps as f64
);
println!("rows={rows}");
println!("batches={count}");
println!("columns={columns}");
Ok(())
}
fn write(source: &str, target: &str, index: bool) -> Result<(), Box<dyn std::error::Error>> {
use geopackage::TableSchemaBuilder;
let src = GeoPackage::open(source)?;
let layer = src.layer("features")?;
let schema = layer.arrow_schema()?;
let batches: Vec<_> = layer
.read_arrow(ArrowReadOptions::default().with_threads(1))?
.collect::<Result<Vec<_>, _>>()?;
let rows: usize = batches.iter().map(arrow_array::RecordBatch::num_rows).sum();
let count = batches.len();
let dst = GeoPackage::create(target)?;
let layer = dst.create_layer(
&TableSchemaBuilder::new("features")
.from_arrow_schema(&schema)?
.spatial_index(false),
)?;
if index {
layer.create_spatial_index()?;
}
drop(layer);
let start = Instant::now();
let written = dst
.layer("features")?
.write_arrow(batches.into_iter().map(Ok), 0)?;
dst.close()?;
let elapsed = start.elapsed();
println!("elapsed_ms={:.3}", elapsed.as_secs_f64() * 1000.0);
println!("rows={}", written.len());
println!("batches={count}");
println!("source_rows={rows}");
println!("index={}", if index { "yes" } else { "no" });
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = std::env::args().collect();
let usage = "usage: arrow_bench <fixture <file> <rows>|noop <file>|read <file> [reps] [threads]|write <src> <dst>>";
let command = args.get(1).map(String::as_str).unwrap_or("");
let path = args.get(2).map(String::as_str).unwrap_or("");
match command {
"fixture" => {
let rows: usize = args.get(3).ok_or(usage)?.parse()?;
fixture(path, rows)
}
"noop" => noop(path),
"write" => {
let target = args.get(3).ok_or(usage)?;
let index = args.get(4).map(String::as_str) == Some("yes");
write(path, target, index)
}
"read" => {
let reps: usize = args.get(3).map_or(Ok(1), |r| r.parse())?;
let threads: usize = args.get(4).map_or(Ok(0), |t| t.parse())?;
read(path, reps.max(1), threads)
}
_ => Err(usage.into()),
}
}