This is an automated email from the ASF dual-hosted git repository.
paleolimbot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sedona-db.git
The following commit(s) were added to refs/heads/main by this push:
new ccaf269 feat(rust/sedona-raster): Add raster schema (#253)
ccaf269 is described below
commit ccaf269223b5334ab9590a40f9b09e182335a0d3
Author: jp <[email protected]>
AuthorDate: Fri Oct 31 07:36:08 2025 -0700
feat(rust/sedona-raster): Add raster schema (#253)
---
Cargo.lock | 11 +
Cargo.toml | 2 +
c/sedona-geoarrow-c/src/geoarrow_c.rs | 5 +
rust/sedona-functions/src/sd_format.rs | 2 +
rust/sedona-raster/Cargo.toml | 35 +
rust/sedona-raster/src/array.rs | 624 +++++++++++++++++
rust/sedona-raster/src/builder.rs | 828 +++++++++++++++++++++++
rust/{sedona-schema => sedona-raster}/src/lib.rs | 8 +-
rust/sedona-raster/src/traits.rs | 120 ++++
rust/sedona-schema/src/datatypes.rs | 31 +
rust/sedona-schema/src/lib.rs | 1 +
rust/sedona-schema/src/raster.rs | 334 +++++++++
12 files changed, 1996 insertions(+), 5 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 291859e..f34de99 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5118,6 +5118,17 @@ dependencies = [
"wkb",
]
+[[package]]
+name = "sedona-raster"
+version = "0.2.0"
+dependencies = [
+ "arrow-array",
+ "arrow-buffer",
+ "arrow-schema",
+ "sedona-common",
+ "sedona-schema",
+]
+
[[package]]
name = "sedona-s2geography"
version = "0.2.0"
diff --git a/Cargo.toml b/Cargo.toml
index e1ac6a6..909676e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,6 +30,7 @@ members = [
"rust/sedona-geo",
"rust/sedona-geometry",
"rust/sedona-geoparquet",
+ "rust/sedona-raster",
"rust/sedona-schema",
"rust/sedona-spatial-join",
"rust/sedona-testing",
@@ -65,6 +66,7 @@ arrow-cast = { version = "56.0.0" }
arrow-data = { version = "56.0.0" }
arrow-json = { version = "56.0.0" }
arrow-schema = { version = "56.0.0" }
+arrow-buffer = { version = "56.0.0" }
async-trait = { version = "0.1.87" }
bytes = "1.10"
byteorder = "1"
diff --git a/c/sedona-geoarrow-c/src/geoarrow_c.rs
b/c/sedona-geoarrow-c/src/geoarrow_c.rs
index 67b2608..d229ba5 100644
--- a/c/sedona-geoarrow-c/src/geoarrow_c.rs
+++ b/c/sedona-geoarrow-c/src/geoarrow_c.rs
@@ -281,6 +281,11 @@ fn geoarrow_type_id(sedona_type: &SedonaType) ->
Result<GeoArrowType, GeoArrowCE
)));
}
},
+ SedonaType::Raster => {
+ return Err(GeoArrowCError::Invalid(
+ "GeoArrow does not support Raster types".to_string(),
+ ))
+ }
};
Ok(type_id)
diff --git a/rust/sedona-functions/src/sd_format.rs
b/rust/sedona-functions/src/sd_format.rs
index b1bb33a..31d11b5 100644
--- a/rust/sedona-functions/src/sd_format.rs
+++ b/rust/sedona-functions/src/sd_format.rs
@@ -149,6 +149,7 @@ fn sedona_type_to_formatted_type(sedona_type: &SedonaType)
-> Result<SedonaType>
_ => Ok(sedona_type.clone()),
}
}
+ SedonaType::Raster => internal_err!("SD_Format does not support Raster
types"),
}
}
@@ -210,6 +211,7 @@ fn columnar_value_to_formatted_value(
},
_ => Ok(columnar_value.clone()),
},
+ SedonaType::Raster => internal_err!("SD_Format does not support Raster
types"),
}
}
diff --git a/rust/sedona-raster/Cargo.toml b/rust/sedona-raster/Cargo.toml
new file mode 100644
index 0000000..91f35dc
--- /dev/null
+++ b/rust/sedona-raster/Cargo.toml
@@ -0,0 +1,35 @@
+# 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.
+[package]
+name = "sedona-raster"
+version.workspace = true
+homepage.workspace = true
+repository.workspace = true
+description.workspace = true
+readme.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+
+[lints.clippy]
+result_large_err = "allow"
+
+[dependencies]
+arrow-schema = { workspace = true }
+arrow-array = { workspace = true }
+arrow-buffer = { workspace = true }
+sedona-common = { path = "../sedona-common" }
+sedona-schema = { path = "../sedona-schema" }
diff --git a/rust/sedona-raster/src/array.rs b/rust/sedona-raster/src/array.rs
new file mode 100644
index 0000000..6e572e4
--- /dev/null
+++ b/rust/sedona-raster/src/array.rs
@@ -0,0 +1,624 @@
+// 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.
+
+use arrow_array::{
+ Array, BinaryArray, BinaryViewArray, Float64Array, ListArray, StringArray,
StringViewArray,
+ StructArray, UInt32Array, UInt64Array,
+};
+use arrow_schema::ArrowError;
+
+use crate::traits::{
+ BandIterator, BandMetadataRef, BandRef, BandsRef, MetadataRef,
RasterMetadata, RasterRef,
+};
+use sedona_schema::raster::{
+ band_indices, band_metadata_indices, metadata_indices, raster_indices,
BandDataType,
+ StorageType,
+};
+
+/// Implement MetadataRef for RasterMetadata to allow direct use with builder
+impl MetadataRef for RasterMetadata {
+ fn width(&self) -> u64 {
+ self.width
+ }
+ fn height(&self) -> u64 {
+ self.height
+ }
+ fn upper_left_x(&self) -> f64 {
+ self.upperleft_x
+ }
+ fn upper_left_y(&self) -> f64 {
+ self.upperleft_y
+ }
+ fn scale_x(&self) -> f64 {
+ self.scale_x
+ }
+ fn scale_y(&self) -> f64 {
+ self.scale_y
+ }
+ fn skew_x(&self) -> f64 {
+ self.skew_x
+ }
+ fn skew_y(&self) -> f64 {
+ self.skew_y
+ }
+}
+
+//
+
+/// Implementation of MetadataRef for Arrow StructArray
+struct MetadataRefImpl<'a> {
+ width_array: &'a UInt64Array,
+ height_array: &'a UInt64Array,
+ upper_left_x_array: &'a Float64Array,
+ upper_left_y_array: &'a Float64Array,
+ scale_x_array: &'a Float64Array,
+ scale_y_array: &'a Float64Array,
+ skew_x_array: &'a Float64Array,
+ skew_y_array: &'a Float64Array,
+ index: usize,
+}
+
+impl<'a> MetadataRef for MetadataRefImpl<'a> {
+ fn width(&self) -> u64 {
+ self.width_array.value(self.index)
+ }
+
+ fn height(&self) -> u64 {
+ self.height_array.value(self.index)
+ }
+
+ fn upper_left_x(&self) -> f64 {
+ self.upper_left_x_array.value(self.index)
+ }
+
+ fn upper_left_y(&self) -> f64 {
+ self.upper_left_y_array.value(self.index)
+ }
+
+ fn scale_x(&self) -> f64 {
+ self.scale_x_array.value(self.index)
+ }
+
+ fn scale_y(&self) -> f64 {
+ self.scale_y_array.value(self.index)
+ }
+
+ fn skew_x(&self) -> f64 {
+ self.skew_x_array.value(self.index)
+ }
+
+ fn skew_y(&self) -> f64 {
+ self.skew_y_array.value(self.index)
+ }
+}
+
+/// Implementation of BandMetadataRef for Arrow StructArray
+struct BandMetadataRefImpl<'a> {
+ nodata_array: &'a BinaryArray,
+ storage_type_array: &'a UInt32Array,
+ datatype_array: &'a UInt32Array,
+ outdb_url_array: &'a StringArray,
+ outdb_band_id_array: &'a UInt32Array,
+ band_index: usize,
+}
+
+impl<'a> BandMetadataRef for BandMetadataRefImpl<'a> {
+ fn nodata_value(&self) -> Option<&[u8]> {
+ if self.nodata_array.is_null(self.band_index) {
+ None
+ } else {
+ Some(self.nodata_array.value(self.band_index))
+ }
+ }
+
+ fn storage_type(&self) -> StorageType {
+ match self.storage_type_array.value(self.band_index) {
+ 0 => StorageType::InDb,
+ 1 => StorageType::OutDbRef,
+ _ => panic!(
+ "Unknown storage type: {}",
+ self.storage_type_array.value(self.band_index)
+ ),
+ }
+ }
+
+ fn data_type(&self) -> BandDataType {
+ match self.datatype_array.value(self.band_index) {
+ 0 => BandDataType::UInt8,
+ 1 => BandDataType::UInt16,
+ 2 => BandDataType::Int16,
+ 3 => BandDataType::UInt32,
+ 4 => BandDataType::Int32,
+ 5 => BandDataType::Float32,
+ 6 => BandDataType::Float64,
+ _ => panic!(
+ "Unknown band data type: {}",
+ self.datatype_array.value(self.band_index)
+ ),
+ }
+ }
+
+ fn outdb_url(&self) -> Option<&str> {
+ if self.outdb_url_array.is_null(self.band_index) {
+ None
+ } else {
+ Some(self.outdb_url_array.value(self.band_index))
+ }
+ }
+
+ fn outdb_band_id(&self) -> Option<u32> {
+ if self.outdb_band_id_array.is_null(self.band_index) {
+ None
+ } else {
+ Some(self.outdb_band_id_array.value(self.band_index))
+ }
+ }
+}
+
+/// Implementation of BandRef for accessing individual band data
+struct BandRefImpl<'a> {
+ band_metadata: BandMetadataRefImpl<'a>,
+ band_data: &'a [u8],
+}
+
+impl<'a> BandRef for BandRefImpl<'a> {
+ fn metadata(&self) -> &dyn BandMetadataRef {
+ &self.band_metadata
+ }
+
+ fn data(&self) -> &[u8] {
+ self.band_data
+ }
+}
+
+/// Implementation of BandsRef for accessing all bands in a raster
+struct BandsRefImpl<'a> {
+ bands_list: &'a ListArray,
+ raster_index: usize,
+ // Direct references to the metadata and data structs
+ band_metadata_struct: &'a StructArray,
+ band_data_array: &'a BinaryViewArray,
+}
+
+impl<'a> BandsRef for BandsRefImpl<'a> {
+ fn len(&self) -> usize {
+ let start = self.bands_list.value_offsets()[self.raster_index] as
usize;
+ let end = self.bands_list.value_offsets()[self.raster_index + 1] as
usize;
+ end - start
+ }
+
+ /// Get a specific band by number (1-based index)
+ fn band(&self, number: usize) -> Result<Box<dyn BandRef + '_>, ArrowError>
{
+ if number == 0 {
+ return Err(ArrowError::InvalidArgumentError(format!(
+ "Invalid band number {}: band numbers must be 1-based",
+ number
+ )));
+ }
+ // By convention, band numbers are 1-based.
+ // Convert to zero-based index.
+ let index = number - 1;
+ if index >= self.len() {
+ return Err(ArrowError::InvalidArgumentError(format!(
+ "Band number {} is out of range: this raster has {} bands",
+ number,
+ self.len()
+ )));
+ }
+
+ let start = self.bands_list.value_offsets()[self.raster_index] as
usize;
+ let band_row = start + index;
+
+ let band_metadata = BandMetadataRefImpl {
+ nodata_array: self
+ .band_metadata_struct
+ .column(band_metadata_indices::NODATAVALUE)
+ .as_any()
+ .downcast_ref::<BinaryArray>()
+ .ok_or(ArrowError::SchemaError(
+ "Failed to downcast nodata to BinaryArray".to_string(),
+ ))?,
+ storage_type_array: self
+ .band_metadata_struct
+ .column(band_metadata_indices::STORAGE_TYPE)
+ .as_any()
+ .downcast_ref::<UInt32Array>()
+ .ok_or(ArrowError::SchemaError(
+ "Failed to downcast storage_type to
UInt32Array".to_string(),
+ ))?,
+ datatype_array: self
+ .band_metadata_struct
+ .column(band_metadata_indices::DATATYPE)
+ .as_any()
+ .downcast_ref::<UInt32Array>()
+ .ok_or(ArrowError::SchemaError(
+ "Failed to downcast datatype to UInt32Array".to_string(),
+ ))?,
+ outdb_url_array: self
+ .band_metadata_struct
+ .column(band_metadata_indices::OUTDB_URL)
+ .as_any()
+ .downcast_ref::<StringArray>()
+ .ok_or(ArrowError::SchemaError(
+ "Failed to downcast outdb_url to StringArray".to_string(),
+ ))?,
+ outdb_band_id_array: self
+ .band_metadata_struct
+ .column(band_metadata_indices::OUTDB_BAND_ID)
+ .as_any()
+ .downcast_ref::<UInt32Array>()
+ .ok_or(ArrowError::SchemaError(
+ "Failed to downcast outdb_band_id to
UInt32Array".to_string(),
+ ))?,
+ band_index: band_row,
+ };
+
+ let band_data = self.band_data_array.value(band_row);
+
+ Ok(Box::new(BandRefImpl {
+ band_metadata,
+ band_data,
+ }))
+ }
+
+ fn iter(&self) -> Box<dyn BandIterator<'_> + '_> {
+ Box::new(BandIteratorImpl {
+ bands: self,
+ current: 1, // Start at 1 for 1-based band numbering
+ })
+ }
+}
+
+/// Concrete implementation of BandIterator trait
+pub struct BandIteratorImpl<'a> {
+ bands: &'a dyn BandsRef,
+ current: usize,
+}
+
+impl<'a> Iterator for BandIteratorImpl<'a> {
+ type Item = Box<dyn BandRef + 'a>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ // current is 1-based, compare against len() + 1
+ if self.current <= self.bands.len() {
+ let band = self.bands.band(self.current).ok(); // Convert Result
to Option
+ self.current += 1;
+ band
+ } else {
+ None
+ }
+ }
+
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ // current is 1-based, so remaining calculation needs adjustment
+ let remaining = self.bands.len().saturating_sub(self.current - 1);
+ (remaining, Some(remaining))
+ }
+}
+
+impl<'a> BandIterator<'a> for BandIteratorImpl<'a> {
+ fn len(&self) -> usize {
+ // current is 1-based, so remaining calculation needs adjustment
+ self.bands.len().saturating_sub(self.current - 1)
+ }
+}
+
+impl ExactSizeIterator for BandIteratorImpl<'_> {}
+
+/// Implementation of RasterRef for complete raster access
+pub struct RasterRefImpl<'a> {
+ metadata: MetadataRefImpl<'a>,
+ crs: &'a StringViewArray,
+ bands: BandsRefImpl<'a>,
+}
+
+impl<'a> RasterRefImpl<'a> {
+ /// Create a new RasterRefImpl from a struct array and index using
hard-coded indices
+ pub fn new(raster_struct: &'a StructArray, raster_index: usize) -> Self {
+ let metadata_struct = raster_struct
+ .column(raster_indices::METADATA)
+ .as_any()
+ .downcast_ref::<StructArray>()
+ .unwrap();
+
+ let crs = raster_struct
+ .column(raster_indices::CRS)
+ .as_any()
+ .downcast_ref::<StringViewArray>()
+ .unwrap();
+
+ let bands_list = raster_struct
+ .column(raster_indices::BANDS)
+ .as_any()
+ .downcast_ref::<ListArray>()
+ .unwrap();
+
+ let metadata = MetadataRefImpl {
+ width_array: metadata_struct
+ .column(metadata_indices::WIDTH)
+ .as_any()
+ .downcast_ref::<UInt64Array>()
+ .unwrap(),
+ height_array: metadata_struct
+ .column(metadata_indices::HEIGHT)
+ .as_any()
+ .downcast_ref::<UInt64Array>()
+ .unwrap(),
+ upper_left_x_array: metadata_struct
+ .column(metadata_indices::UPPERLEFT_X)
+ .as_any()
+ .downcast_ref::<Float64Array>()
+ .unwrap(),
+ upper_left_y_array: metadata_struct
+ .column(metadata_indices::UPPERLEFT_Y)
+ .as_any()
+ .downcast_ref::<Float64Array>()
+ .unwrap(),
+ scale_x_array: metadata_struct
+ .column(metadata_indices::SCALE_X)
+ .as_any()
+ .downcast_ref::<Float64Array>()
+ .unwrap(),
+ scale_y_array: metadata_struct
+ .column(metadata_indices::SCALE_Y)
+ .as_any()
+ .downcast_ref::<Float64Array>()
+ .unwrap(),
+ skew_x_array: metadata_struct
+ .column(metadata_indices::SKEW_X)
+ .as_any()
+ .downcast_ref::<Float64Array>()
+ .unwrap(),
+ skew_y_array: metadata_struct
+ .column(metadata_indices::SKEW_Y)
+ .as_any()
+ .downcast_ref::<Float64Array>()
+ .unwrap(),
+ index: raster_index,
+ };
+
+ // Extract the band structs for direct access
+ let bands_struct = bands_list
+ .values()
+ .as_any()
+ .downcast_ref::<StructArray>()
+ .unwrap();
+
+ let band_metadata_struct = bands_struct
+ .column(band_indices::METADATA)
+ .as_any()
+ .downcast_ref::<StructArray>()
+ .unwrap();
+
+ let band_data_array = bands_struct
+ .column(band_indices::DATA)
+ .as_any()
+ .downcast_ref::<BinaryViewArray>()
+ .unwrap();
+
+ let bands = BandsRefImpl {
+ bands_list,
+ raster_index,
+ band_metadata_struct,
+ band_data_array,
+ };
+
+ Self {
+ metadata,
+ crs,
+ bands,
+ }
+ }
+}
+
+impl<'a> RasterRef for RasterRefImpl<'a> {
+ fn metadata(&self) -> &dyn MetadataRef {
+ &self.metadata
+ }
+
+ fn crs(&self) -> Option<&str> {
+ if self.crs.is_null(self.bands.raster_index) {
+ None
+ } else {
+ Some(self.crs.value(self.bands.raster_index))
+ }
+ }
+
+ fn bands(&self) -> &dyn BandsRef {
+ &self.bands
+ }
+}
+
+/// Access rasters from the Arrow StructArray
+///
+/// This provides efficient, zero-copy access to raster data stored in Arrow
format.
+pub struct RasterStructArray<'a> {
+ raster_array: &'a StructArray,
+}
+
+impl<'a> RasterStructArray<'a> {
+ /// Create a new RasterStructArray from an existing StructArray
+ pub fn new(raster_array: &'a StructArray) -> Self {
+ Self { raster_array }
+ }
+
+ /// Get the total number of rasters in the array
+ pub fn len(&self) -> usize {
+ self.raster_array.len()
+ }
+
+ /// Check if the array is empty
+ pub fn is_empty(&self) -> bool {
+ self.raster_array.is_empty()
+ }
+
+ /// Get a specific raster by index without consuming the iterator
+ pub fn get(&self, index: usize) -> Option<RasterRefImpl<'a>> {
+ if index >= self.raster_array.len() {
+ return None;
+ }
+
+ Some(RasterRefImpl::new(self.raster_array, index))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::builder::RasterBuilder;
+ use crate::traits::{BandMetadata, RasterMetadata};
+ use sedona_schema::raster::{BandDataType, StorageType};
+
+ #[test]
+ fn test_array_basic_functionality() {
+ // Create a simple raster for testing using the correct API
+ let mut builder = RasterBuilder::new(10); // capacity
+
+ let metadata = RasterMetadata {
+ width: 10,
+ height: 10,
+ upperleft_x: 0.0,
+ upperleft_y: 0.0,
+ scale_x: 1.0,
+ scale_y: -1.0,
+ skew_x: 0.0,
+ skew_y: 0.0,
+ };
+
+ let epsg4326 = "EPSG:4326";
+
+ builder.start_raster(&metadata, Some(epsg4326)).unwrap();
+
+ let band_metadata = BandMetadata {
+ nodata_value: Some(vec![255u8]),
+ storage_type: StorageType::InDb,
+ datatype: BandDataType::UInt8,
+ outdb_url: None,
+ outdb_band_id: None,
+ };
+
+ // Add a single band with some test data using the correct API
+ builder.start_band(band_metadata.clone()).unwrap();
+ let test_data = vec![1u8; 100]; // 10x10 raster with value 1
+ builder.band_data_writer().append_value(&test_data);
+ builder.finish_band().unwrap();
+ let result = builder.finish_raster();
+ assert!(result.is_ok());
+
+ let raster_array = builder.finish().unwrap();
+
+ // Test the array
+ let rasters = RasterStructArray::new(&raster_array);
+
+ assert_eq!(rasters.len(), 1);
+ assert!(!rasters.is_empty());
+
+ let raster = rasters.get(0).unwrap();
+ let metadata = raster.metadata();
+
+ assert_eq!(metadata.width(), 10);
+ assert_eq!(metadata.height(), 10);
+ assert_eq!(metadata.scale_x(), 1.0);
+ assert_eq!(metadata.scale_y(), -1.0);
+
+ let bands = raster.bands();
+ assert_eq!(bands.len(), 1);
+ assert!(!bands.is_empty());
+
+ // Access band with 1-based band_number
+ let band = bands.band(1).unwrap();
+ assert_eq!(band.data().len(), 100);
+ assert_eq!(band.data()[0], 1u8);
+
+ let band_meta = band.metadata();
+ assert_eq!(band_meta.storage_type(), StorageType::InDb);
+ assert_eq!(band_meta.data_type(), BandDataType::UInt8);
+
+ let crs = raster.crs().unwrap();
+ assert_eq!(crs, epsg4326);
+
+ // Test array over bands
+ let band_iter: Vec<_> = bands.iter().collect();
+ assert_eq!(band_iter.len(), 1);
+ }
+
+ #[test]
+ fn test_multi_band_array() {
+ let mut builder = RasterBuilder::new(3);
+
+ let metadata = RasterMetadata {
+ width: 5,
+ height: 5,
+ upperleft_x: 0.0,
+ upperleft_y: 0.0,
+ scale_x: 1.0,
+ scale_y: -1.0,
+ skew_x: 0.0,
+ skew_y: 0.0,
+ };
+
+ builder.start_raster(&metadata, None).unwrap();
+
+ // Add three bands using the correct API
+ for band_idx in 0..3 {
+ let band_metadata = BandMetadata {
+ nodata_value: Some(vec![255u8]),
+ storage_type: StorageType::InDb,
+ datatype: BandDataType::UInt8,
+ outdb_url: None,
+ outdb_band_id: None,
+ };
+
+ builder.start_band(band_metadata).unwrap();
+ let test_data = vec![band_idx as u8; 25]; // 5x5 raster
+ builder.band_data_writer().append_value(&test_data);
+ builder.finish_band().unwrap();
+ }
+
+ let result = builder.finish_raster();
+ assert!(result.is_ok());
+
+ let raster_array = builder.finish().unwrap();
+
+ let rasters = RasterStructArray::new(&raster_array);
+ let raster = rasters.get(0).unwrap();
+ let bands = raster.bands();
+
+ assert_eq!(bands.len(), 3);
+
+ // Test each band has different data
+ // Use 1-based band numbers
+ for i in 0..3 {
+ // Access band with 1-based band_number
+ let band = bands.band(i + 1).unwrap();
+ let expected_value = i as u8;
+ assert!(band.data().iter().all(|&x| x == expected_value));
+ }
+
+ // Test array
+ let band_values: Vec<u8> = bands
+ .iter()
+ .enumerate()
+ .map(|(i, band)| {
+ assert_eq!(band.data()[0], i as u8);
+ band.data()[0]
+ })
+ .collect();
+
+ assert_eq!(band_values, vec![0, 1, 2]);
+ }
+}
diff --git a/rust/sedona-raster/src/builder.rs
b/rust/sedona-raster/src/builder.rs
new file mode 100644
index 0000000..357486a
--- /dev/null
+++ b/rust/sedona-raster/src/builder.rs
@@ -0,0 +1,828 @@
+// 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.
+
+use arrow_array::{
+ builder::{
+ BinaryBuilder, BinaryViewBuilder, BooleanBuilder, Float64Builder,
StringBuilder,
+ StringViewBuilder, UInt32Builder, UInt64Builder,
+ },
+ Array, ArrayRef, ListArray, StructArray,
+};
+use arrow_buffer::{OffsetBuffer, ScalarBuffer};
+use arrow_schema::{ArrowError, DataType};
+use std::sync::Arc;
+
+use sedona_schema::raster::RasterSchema;
+
+use crate::traits::{BandMetadata, MetadataRef};
+
+/// Builder for constructing raster arrays with zero-copy band data writing
+///
+/// Required steps to build a raster:
+/// 1. Create a RasterBuilder with a specified capacity
+/// 2. For each raster to add:
+/// - Call `start_raster` with the appropriate metadata, CRS
+/// - For each band in the raster:
+/// - Call `start_band` with the band metadata
+/// - Use `band_data_writer` to get a BinaryViewBuilder and write the
band data
+/// - Call `finish_band` to complete the band
+/// - Call `finish_raster` to complete the raster
+/// 3. After all rasters are added, call `finish` to get the final StructArray
+///
+/// Example usage:
+/// ```
+/// use sedona_raster::traits::{RasterMetadata, BandMetadata};
+/// use sedona_schema::raster::{StorageType, BandDataType};
+/// use sedona_raster::builder::RasterBuilder;
+///
+/// let mut builder = RasterBuilder::new(1);
+/// let metadata = RasterMetadata {
+/// width: 100, height: 100,
+/// upperleft_x: 0.0, upperleft_y: 0.0,
+/// scale_x: 1.0, scale_y: -1.0,
+/// skew_x: 0.0, skew_y: 0.0,
+/// };
+/// // Start a raster from RasterMetadata struct
+/// builder.start_raster(&metadata, Some("EPSG:4326")).unwrap();
+///
+/// // Add a band:
+/// let band_metadata = BandMetadata {
+/// nodata_value: Some(vec![0u8]),
+/// storage_type: StorageType::InDb,
+/// datatype: BandDataType::UInt8,
+/// outdb_url: None,
+/// outdb_band_id: None,
+/// };
+/// builder.start_band(band_metadata).unwrap();
+/// let band_writer = builder.band_data_writer();
+/// band_writer.append_value(&vec![/* band data bytes */]);
+/// builder.finish_band().unwrap();
+///
+/// // Finish the raster
+/// builder.finish_raster().unwrap();
+///
+/// // Finish building and get the StructArray
+/// let raster_array = builder.finish().unwrap();
+/// ```
+pub struct RasterBuilder {
+ // Metadata fields
+ width: UInt64Builder,
+ height: UInt64Builder,
+ upper_left_x: Float64Builder,
+ upper_left_y: Float64Builder,
+ scale_x: Float64Builder,
+ scale_y: Float64Builder,
+ skew_x: Float64Builder,
+ skew_y: Float64Builder,
+
+ // CRS field
+ crs: StringViewBuilder,
+
+ // Band metadata fields
+ band_nodata: BinaryBuilder,
+ band_storage_type: UInt32Builder,
+ band_datatype: UInt32Builder,
+ band_outdb_url: StringBuilder,
+ band_outdb_band_id: UInt32Builder,
+
+ // Band data field
+ band_data: BinaryViewBuilder,
+
+ // List structure tracking
+ band_offsets: Vec<i32>, // Track where each raster's bands start/end
+ current_band_count: i32, // Track bands in current raster
+
+ raster_validity: BooleanBuilder, // Track which rasters are null
+}
+
+impl RasterBuilder {
+ /// Create a new raster builder with the specified capacity
+ pub fn new(capacity: usize) -> Self {
+ Self {
+ // Metadata builders
+ width: UInt64Builder::with_capacity(capacity),
+ height: UInt64Builder::with_capacity(capacity),
+ upper_left_x: Float64Builder::with_capacity(capacity),
+ upper_left_y: Float64Builder::with_capacity(capacity),
+ scale_x: Float64Builder::with_capacity(capacity),
+ scale_y: Float64Builder::with_capacity(capacity),
+ skew_x: Float64Builder::with_capacity(capacity),
+ skew_y: Float64Builder::with_capacity(capacity),
+
+ // CRS builder
+ crs: StringViewBuilder::with_capacity(capacity),
+
+ // Band builders - estimate some bands per raster
+ // The capacity is at raster level, but each raster has multiple
bands and
+ // are large. We may want to add an optional parameter to control
expected
+ // bands per raster or even band size in the future
+ band_nodata: BinaryBuilder::with_capacity(capacity, capacity),
+ band_storage_type: UInt32Builder::with_capacity(capacity),
+ band_datatype: UInt32Builder::with_capacity(capacity),
+ band_outdb_url: StringBuilder::with_capacity(capacity, capacity),
+ band_outdb_band_id: UInt32Builder::with_capacity(capacity),
+ band_data: BinaryViewBuilder::with_capacity(capacity),
+
+ // List tracking
+ band_offsets: vec![0],
+ current_band_count: 0,
+
+ // Raster-level validity (keeps track of null rasters)
+ raster_validity: BooleanBuilder::with_capacity(capacity),
+ }
+ }
+
+ /// Start a new raster with metadata and optional CRS
+ pub fn start_raster(
+ &mut self,
+ metadata: &dyn MetadataRef,
+ crs: Option<&str>,
+ ) -> Result<(), ArrowError> {
+ self.append_metadata_from_ref(metadata)?;
+ self.append_crs(crs)?;
+
+ // Reset band count for this raster
+ self.current_band_count = 0;
+
+ Ok(())
+ }
+
+ /// Start a new band - this must be called before writing band data
+ pub fn start_band(&mut self, band_metadata: BandMetadata) -> Result<(),
ArrowError> {
+ // Append band metadata
+ match band_metadata.nodata_value {
+ Some(nodata) => self.band_nodata.append_value(&nodata),
+ None => self.band_nodata.append_null(),
+ }
+
+ self.band_storage_type
+ .append_value(band_metadata.storage_type as u32);
+ self.band_datatype
+ .append_value(band_metadata.datatype as u32);
+
+ match band_metadata.outdb_url {
+ Some(url) => self.band_outdb_url.append_value(&url),
+ None => self.band_outdb_url.append_null(),
+ }
+
+ match band_metadata.outdb_band_id {
+ Some(band_id) => self.band_outdb_band_id.append_value(band_id),
+ None => self.band_outdb_band_id.append_null(),
+ }
+
+ self.current_band_count += 1;
+
+ Ok(())
+ }
+
+ /// Get direct access to the BinaryViewBuilder for writing the current
band's data
+ /// Must be called after start_band() to write data to the current band
+ pub fn band_data_writer(&mut self) -> &mut BinaryViewBuilder {
+ &mut self.band_data
+ }
+
+ /// Finish writing the current band
+ pub fn finish_band(&mut self) -> Result<(), ArrowError> {
+ // Band data should already be written via band_data_writer
+ // Nothing additional needed here since we're building flat
+ Ok(())
+ }
+
+ /// Finish all bands for the current raster
+ pub fn finish_raster(&mut self) -> Result<(), ArrowError> {
+ // Record the end offset for this raster's bands
+ let next_offset = self.band_offsets.last().unwrap() +
self.current_band_count;
+ self.band_offsets.push(next_offset);
+
+ self.raster_validity.append_value(true);
+
+ Ok(())
+ }
+
+ /// Append raster metadata from a MetadataRef trait object
+ fn append_metadata_from_ref(&mut self, metadata: &dyn MetadataRef) ->
Result<(), ArrowError> {
+ self.width.append_value(metadata.width());
+ self.height.append_value(metadata.height());
+ self.upper_left_x.append_value(metadata.upper_left_x());
+ self.upper_left_y.append_value(metadata.upper_left_y());
+ self.scale_x.append_value(metadata.scale_x());
+ self.scale_y.append_value(metadata.scale_y());
+ self.skew_x.append_value(metadata.skew_x());
+ self.skew_y.append_value(metadata.skew_y());
+
+ Ok(())
+ }
+
+ /// Set the CRS for the current raster
+ pub fn append_crs(&mut self, crs: Option<&str>) -> Result<(), ArrowError> {
+ match crs {
+ Some(crs_data) => self.crs.append_value(crs_data),
+ None => self.crs.append_null(),
+ }
+ Ok(())
+ }
+
+ /// Append a null raster
+ pub fn append_null(&mut self) -> Result<(), ArrowError> {
+ // Since metadata fields are non-nullable, provide default values
+ self.width.append_value(0u64);
+ self.height.append_value(0u64);
+ self.upper_left_x.append_value(0.0f64);
+ self.upper_left_y.append_value(0.0f64);
+ self.scale_x.append_value(0.0f64);
+ self.scale_y.append_value(0.0f64);
+ self.skew_x.append_value(0.0f64);
+ self.skew_y.append_value(0.0f64);
+
+ // Append null CRS
+ self.crs.append_null();
+
+ // No bands for null raster
+ let current_offset = *self.band_offsets.last().unwrap();
+ self.band_offsets.push(current_offset);
+
+ // Mark raster as null
+ self.raster_validity.append_value(false);
+
+ Ok(())
+ }
+
+ /// Finish building and return the constructed StructArray
+ pub fn finish(mut self) -> Result<StructArray, ArrowError> {
+ // Build the metadata struct using the schema
+ let metadata_fields = if let DataType::Struct(fields) =
RasterSchema::metadata_type() {
+ fields
+ } else {
+ return Err(ArrowError::SchemaError(
+ "Expected struct type for metadata".to_string(),
+ ));
+ };
+
+ let metadata_arrays: Vec<ArrayRef> = vec![
+ Arc::new(self.width.finish()),
+ Arc::new(self.height.finish()),
+ Arc::new(self.upper_left_x.finish()),
+ Arc::new(self.upper_left_y.finish()),
+ Arc::new(self.scale_x.finish()),
+ Arc::new(self.scale_y.finish()),
+ Arc::new(self.skew_x.finish()),
+ Arc::new(self.skew_y.finish()),
+ ];
+ let metadata_array = StructArray::new(metadata_fields,
metadata_arrays, None);
+
+ // Build the band metadata struct using the schema
+ let band_metadata_fields =
+ if let DataType::Struct(fields) =
RasterSchema::band_metadata_type() {
+ fields
+ } else {
+ return Err(ArrowError::SchemaError(
+ "Expected struct type for band metadata".to_string(),
+ ));
+ };
+
+ let band_metadata_arrays: Vec<ArrayRef> = vec![
+ Arc::new(self.band_nodata.finish()),
+ Arc::new(self.band_storage_type.finish()),
+ Arc::new(self.band_datatype.finish()),
+ Arc::new(self.band_outdb_url.finish()),
+ Arc::new(self.band_outdb_band_id.finish()),
+ ];
+ let band_metadata_array =
+ StructArray::new(band_metadata_fields, band_metadata_arrays, None);
+
+ // Build the band struct using the schema
+ let band_fields = if let DataType::Struct(fields) =
RasterSchema::band_type() {
+ fields
+ } else {
+ return Err(ArrowError::SchemaError(
+ "Expected struct type for band".to_string(),
+ ));
+ };
+
+ let band_arrays: Vec<ArrayRef> = vec![
+ Arc::new(band_metadata_array),
+ Arc::new(self.band_data.finish()),
+ ];
+ let band_struct_array = StructArray::new(band_fields, band_arrays,
None);
+
+ // Build the bands list array using the schema
+ let band_field = if let DataType::List(field) =
RasterSchema::bands_type() {
+ field
+ } else {
+ return Err(ArrowError::SchemaError(
+ "Expected list type for bands".to_string(),
+ ));
+ };
+
+ let offsets = OffsetBuffer::new(ScalarBuffer::from(self.band_offsets));
+ let bands_list = ListArray::new(band_field, offsets,
Arc::new(band_struct_array), None);
+
+ // Build the final raster struct using the schema
+ let raster_fields = RasterSchema::fields();
+ let raster_arrays: Vec<ArrayRef> = vec![
+ Arc::new(metadata_array),
+ Arc::new(self.crs.finish()),
+ Arc::new(bands_list),
+ ];
+
+ let raster_validity_array = self.raster_validity.finish();
+ let raster_nulls = raster_validity_array.nulls().cloned();
+
+ Ok(StructArray::new(raster_fields, raster_arrays, raster_nulls))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::array::RasterStructArray;
+ use crate::traits::{RasterMetadata, RasterRef};
+ use sedona_schema::raster::{BandDataType, StorageType};
+
+ #[test]
+ fn test_iterator_basic_functionality() {
+ // Create a simple raster for testing using the correct API
+ let mut builder = RasterBuilder::new(10); // capacity
+
+ let metadata = RasterMetadata {
+ width: 10,
+ height: 10,
+ upperleft_x: 0.0,
+ upperleft_y: 0.0,
+ scale_x: 1.0,
+ scale_y: -1.0,
+ skew_x: 0.0,
+ skew_y: 0.0,
+ };
+
+ let epsg4326 = "EPSG:4326";
+ builder.start_raster(&metadata, Some(epsg4326)).unwrap();
+
+ let band_metadata = BandMetadata {
+ nodata_value: Some(vec![255u8]),
+ storage_type: StorageType::InDb,
+ datatype: BandDataType::UInt8,
+ outdb_url: None,
+ outdb_band_id: None,
+ };
+
+ // Add a single band with some test data using the correct API
+ builder.start_band(band_metadata.clone()).unwrap();
+ let test_data = vec![1u8; 100]; // 10x10 raster with value 1
+ builder.band_data_writer().append_value(&test_data);
+ builder.finish_band().unwrap();
+ let result = builder.finish_raster();
+ assert!(result.is_ok());
+
+ let raster_array = builder.finish().unwrap();
+
+ // Test the iterator
+ let rasters = RasterStructArray::new(&raster_array);
+
+ assert_eq!(rasters.len(), 1);
+ assert!(!rasters.is_empty());
+
+ let raster = rasters.get(0).unwrap();
+ let metadata = raster.metadata();
+
+ assert_eq!(metadata.width(), 10);
+ assert_eq!(metadata.height(), 10);
+ assert_eq!(metadata.scale_x(), 1.0);
+ assert_eq!(metadata.scale_y(), -1.0);
+
+ let bands = raster.bands();
+ assert_eq!(bands.len(), 1);
+ assert!(!bands.is_empty());
+
+ // Access band with 1-based band_number
+ let band = bands.band(1).unwrap();
+ assert_eq!(band.data().len(), 100);
+ assert_eq!(band.data()[0], 1u8);
+
+ let band_meta = band.metadata();
+ assert_eq!(band_meta.storage_type(), StorageType::InDb);
+ assert_eq!(band_meta.data_type(), BandDataType::UInt8);
+
+ let crs = raster.crs().unwrap();
+ assert_eq!(crs, epsg4326);
+
+ // Test iterator over bands
+ let band_iter: Vec<_> = bands.iter().collect();
+ assert_eq!(band_iter.len(), 1);
+ }
+
+ #[test]
+ fn test_multi_band_iterator() {
+ let mut builder = RasterBuilder::new(3);
+
+ let metadata = RasterMetadata {
+ width: 5,
+ height: 5,
+ upperleft_x: 0.0,
+ upperleft_y: 0.0,
+ scale_x: 1.0,
+ scale_y: -1.0,
+ skew_x: 0.0,
+ skew_y: 0.0,
+ };
+
+ builder.start_raster(&metadata, None).unwrap();
+
+ // Add three bands using the correct API
+ for band_idx in 0..3 {
+ let band_metadata = BandMetadata {
+ nodata_value: Some(vec![255u8]),
+ storage_type: StorageType::InDb,
+ datatype: BandDataType::UInt8,
+ outdb_url: None,
+ outdb_band_id: None,
+ };
+
+ builder.start_band(band_metadata).unwrap();
+ let test_data = vec![band_idx as u8; 25]; // 5x5 raster
+ builder.band_data_writer().append_value(&test_data);
+ builder.finish_band().unwrap();
+ }
+
+ let result = builder.finish_raster();
+ assert!(result.is_ok());
+
+ let raster_array = builder.finish().unwrap();
+
+ let rasters = RasterStructArray::new(&raster_array);
+ let raster = rasters.get(0).unwrap();
+ let bands = raster.bands();
+
+ assert_eq!(bands.len(), 3);
+
+ // Test each band has different data
+ // Use 1-based band numbers
+ for i in 0..3 {
+ // Access band with 1-based band_number
+ let band = bands.band(i + 1).unwrap();
+ let expected_value = i as u8;
+ assert!(band.data().iter().all(|&x| x == expected_value));
+ }
+
+ // Test iterator
+ let band_values: Vec<u8> = bands
+ .iter()
+ .enumerate()
+ .map(|(i, band)| {
+ assert_eq!(band.data()[0], i as u8);
+ band.data()[0]
+ })
+ .collect();
+
+ assert_eq!(band_values, vec![0, 1, 2]);
+ }
+
+ #[test]
+ fn test_copy_metadata_from_iterator() {
+ // Create an original raster
+ let mut source_builder = RasterBuilder::new(10);
+
+ let original_metadata = RasterMetadata {
+ width: 42,
+ height: 24,
+ upperleft_x: -122.0,
+ upperleft_y: 37.8,
+ scale_x: 0.1,
+ scale_y: -0.1,
+ skew_x: 0.0,
+ skew_y: 0.0,
+ };
+
+ source_builder
+ .start_raster(&original_metadata, None)
+ .unwrap();
+
+ let band_metadata = BandMetadata {
+ nodata_value: Some(vec![255u8]),
+ storage_type: StorageType::InDb,
+ datatype: BandDataType::UInt8,
+ outdb_url: None,
+ outdb_band_id: None,
+ };
+
+ source_builder.start_band(band_metadata).unwrap();
+ let test_data = vec![42u8; 1008]; // 42x24 raster
+ source_builder.band_data_writer().append_value(&test_data);
+ source_builder.finish_band().unwrap();
+ source_builder.finish_raster().unwrap();
+
+ let source_array = source_builder.finish().unwrap();
+
+ // Create a new raster using metadata from the iterator
+ let mut target_builder = RasterBuilder::new(10);
+ let iterator = RasterStructArray::new(&source_array);
+ let source_raster = iterator.get(0).unwrap();
+
+ target_builder
+ .start_raster(source_raster.metadata(), source_raster.crs())
+ .unwrap();
+
+ // Add new band data while preserving original metadata
+ let new_band_metadata = BandMetadata {
+ nodata_value: None,
+ storage_type: StorageType::InDb,
+ datatype: BandDataType::UInt16,
+ outdb_url: None,
+ outdb_band_id: None,
+ };
+
+ target_builder.start_band(new_band_metadata).unwrap();
+ let new_data = vec![100u16; 1008]; // Different data, same dimensions
+ let new_data_bytes: Vec<u8> = new_data.iter().flat_map(|&x|
x.to_le_bytes()).collect();
+
+ target_builder
+ .band_data_writer()
+ .append_value(&new_data_bytes);
+ target_builder.finish_band().unwrap();
+ target_builder.finish_raster().unwrap();
+
+ let target_array = target_builder.finish().unwrap();
+
+ // Verify the metadata was copied correctly
+ let target_iterator = RasterStructArray::new(&target_array);
+ let target_raster = target_iterator.get(0).unwrap();
+ let target_metadata = target_raster.metadata();
+
+ // All metadata should match the original
+ assert_eq!(target_metadata.width(), 42);
+ assert_eq!(target_metadata.height(), 24);
+ assert_eq!(target_metadata.upper_left_x(), -122.0);
+ assert_eq!(target_metadata.upper_left_y(), 37.8);
+ assert_eq!(target_metadata.scale_x(), 0.1);
+ assert_eq!(target_metadata.scale_y(), -0.1);
+
+ // But band data and metadata should be different
+ let target_band = target_raster.bands().band(1).unwrap();
+ let target_band_meta = target_band.metadata();
+ assert_eq!(target_band_meta.data_type(), BandDataType::UInt16);
+ assert!(target_band_meta.nodata_value().is_none());
+ assert_eq!(target_band.data().len(), 2016); // 1008 * 2 bytes per u16
+
+ let result = target_raster.bands().band(0);
+ assert!(result.is_err(), "Band number 0 should be invalid");
+
+ let result = target_raster.bands().band(2);
+ assert!(result.is_err(), "Band number 2 should be out of range");
+ }
+
+ #[test]
+ fn test_band_data_types() {
+ // Create a test raster with bands of different data types
+ let mut builder = RasterBuilder::new(1);
+
+ let metadata = RasterMetadata {
+ width: 2,
+ height: 2,
+ upperleft_x: 0.0,
+ upperleft_y: 0.0,
+ scale_x: 1.0,
+ scale_y: -1.0,
+ skew_x: 0.0,
+ skew_y: 0.0,
+ };
+
+ builder.start_raster(&metadata, None).unwrap();
+
+ // Test all BandDataType variants
+ let test_cases = vec![
+ (BandDataType::UInt8, vec![1u8, 2u8, 3u8, 4u8]),
+ (
+ BandDataType::UInt16,
+ vec![1u8, 0u8, 2u8, 0u8, 3u8, 0u8, 4u8, 0u8],
+ ), // little-endian u16
+ (
+ BandDataType::Int16,
+ vec![255u8, 255u8, 254u8, 255u8, 253u8, 255u8, 252u8, 255u8],
+ ), // little-endian i16
+ (
+ BandDataType::UInt32,
+ vec![
+ 1u8, 0u8, 0u8, 0u8, 2u8, 0u8, 0u8, 0u8, 3u8, 0u8, 0u8,
0u8, 4u8, 0u8, 0u8, 0u8,
+ ],
+ ), // little-endian u32
+ (
+ BandDataType::Int32,
+ vec![
+ 255u8, 255u8, 255u8, 255u8, 254u8, 255u8, 255u8, 255u8,
253u8, 255u8, 255u8,
+ 255u8, 252u8, 255u8, 255u8, 255u8,
+ ],
+ ), // little-endian i32
+ (
+ BandDataType::Float32,
+ vec![
+ 0u8, 0u8, 128u8, 63u8, 0u8, 0u8, 0u8, 64u8, 0u8, 0u8,
64u8, 64u8, 0u8, 0u8,
+ 128u8, 64u8,
+ ],
+ ), // little-endian f32: 1.0, 2.0, 3.0, 4.0
+ (
+ BandDataType::Float64,
+ vec![
+ 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 240u8, 63u8, 0u8, 0u8, 0u8,
0u8, 0u8, 0u8, 0u8,
+ 64u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 8u8, 64u8, 0u8, 0u8,
0u8, 0u8, 0u8, 0u8,
+ 16u8, 64u8,
+ ],
+ ), // little-endian f64: 1.0, 2.0, 3.0, 4.0
+ ];
+
+ for (expected_data_type, test_data) in test_cases {
+ let band_metadata = BandMetadata {
+ nodata_value: None,
+ storage_type: StorageType::InDb,
+ datatype: expected_data_type.clone(),
+ outdb_url: None,
+ outdb_band_id: None,
+ };
+
+ builder.start_band(band_metadata).unwrap();
+ builder.band_data_writer().append_value(&test_data);
+ builder.finish_band().unwrap();
+ }
+
+ builder.finish_raster().unwrap();
+ let raster_array = builder.finish().unwrap();
+
+ // Test the data type conversion for each band
+ let iterator = RasterStructArray::new(&raster_array);
+ let raster = iterator.get(0).unwrap();
+ let bands = raster.bands();
+
+ assert_eq!(bands.len(), 7, "Expected 7 bands for all data types");
+
+ // Verify each band returns the correct data type
+ let expected_types = [
+ BandDataType::UInt8,
+ BandDataType::UInt16,
+ BandDataType::Int16,
+ BandDataType::UInt32,
+ BandDataType::Int32,
+ BandDataType::Float32,
+ BandDataType::Float64,
+ ];
+
+ // i is zero-based index
+ for (i, expected_type) in expected_types.iter().enumerate() {
+ // Bands are 1-based band_number
+ let band = bands.band(i + 1).unwrap();
+ let band_metadata = band.metadata();
+ let actual_type = band_metadata.data_type();
+
+ assert_eq!(
+ actual_type, *expected_type,
+ "Band {} expected data type {:?}, got {:?}",
+ i, expected_type, actual_type
+ );
+ }
+ }
+
+ #[test]
+ fn test_outdb_metadata_fields() {
+ // Test creating raster with OutDb reference metadata
+ let mut builder = RasterBuilder::new(10);
+
+ let metadata = RasterMetadata {
+ width: 1024,
+ height: 1024,
+ upperleft_x: 0.0,
+ upperleft_y: 0.0,
+ scale_x: 1.0,
+ scale_y: -1.0,
+ skew_x: 0.0,
+ skew_y: 0.0,
+ };
+
+ builder.start_raster(&metadata, None).unwrap();
+
+ // Test InDb band (should have null OutDb fields)
+ let indb_band_metadata = BandMetadata {
+ nodata_value: Some(vec![255u8]),
+ storage_type: StorageType::InDb,
+ datatype: BandDataType::UInt8,
+ outdb_url: None,
+ outdb_band_id: None,
+ };
+
+ builder.start_band(indb_band_metadata).unwrap();
+ let test_data = vec![1u8; 100];
+ builder.band_data_writer().append_value(&test_data);
+ builder.finish_band().unwrap();
+
+ // Test OutDbRef band (should have OutDb fields populated)
+ let outdb_band_metadata = BandMetadata {
+ nodata_value: None,
+ storage_type: StorageType::OutDbRef,
+ datatype: BandDataType::Float32,
+ outdb_url: Some("s3://mybucket/satellite_image.tif".to_string()),
+ outdb_band_id: Some(2),
+ };
+
+ builder.start_band(outdb_band_metadata).unwrap();
+ // For OutDbRef, data field could be empty or contain
metadata/thumbnail
+ builder.band_data_writer().append_value([]);
+ builder.finish_band().unwrap();
+
+ builder.finish_raster().unwrap();
+ let raster_array = builder.finish().unwrap();
+
+ // Verify the band metadata
+ let iterator = RasterStructArray::new(&raster_array);
+ let raster = iterator.get(0).unwrap();
+ let bands = raster.bands();
+
+ assert_eq!(bands.len(), 2);
+
+ // Test InDb band
+ let indb_band = bands.band(1).unwrap();
+ let indb_metadata = indb_band.metadata();
+ assert_eq!(indb_metadata.storage_type(), StorageType::InDb);
+ assert_eq!(indb_metadata.data_type(), BandDataType::UInt8);
+ assert!(indb_metadata.outdb_url().is_none());
+ assert!(indb_metadata.outdb_band_id().is_none());
+ assert_eq!(indb_band.data().len(), 100);
+
+ // Test OutDbRef band
+ let outdb_band = bands.band(2).unwrap();
+ let outdb_metadata = outdb_band.metadata();
+ assert_eq!(outdb_metadata.storage_type(), StorageType::OutDbRef);
+ assert_eq!(outdb_metadata.data_type(), BandDataType::Float32);
+ assert_eq!(
+ outdb_metadata.outdb_url().unwrap(),
+ "s3://mybucket/satellite_image.tif"
+ );
+ assert_eq!(outdb_metadata.outdb_band_id().unwrap(), 2);
+ assert_eq!(outdb_band.data().len(), 0); // Empty data for OutDbRef
+ }
+
+ #[test]
+ fn test_band_access_errors() {
+ // Create a simple raster with one band
+ let mut builder = RasterBuilder::new(1);
+
+ let metadata = RasterMetadata {
+ width: 10,
+ height: 10,
+ upperleft_x: 0.0,
+ upperleft_y: 0.0,
+ scale_x: 1.0,
+ scale_y: -1.0,
+ skew_x: 0.0,
+ skew_y: 0.0,
+ };
+
+ builder.start_raster(&metadata, None).unwrap();
+
+ let band_metadata = BandMetadata {
+ nodata_value: None,
+ storage_type: StorageType::InDb,
+ datatype: BandDataType::UInt8,
+ outdb_url: None,
+ outdb_band_id: None,
+ };
+
+ builder.start_band(band_metadata).unwrap();
+ builder.band_data_writer().append_value([1u8; 100]);
+ builder.finish_band().unwrap();
+ builder.finish_raster().unwrap();
+
+ let raster_array = builder.finish().unwrap();
+ let iterator = RasterStructArray::new(&raster_array);
+ let raster = iterator.get(0).unwrap();
+ let bands = raster.bands();
+
+ // Test invalid band number (0-based)
+ let result = bands.band(0);
+ assert!(result.is_err());
+ let err = result.err().unwrap().to_string();
+ assert!(err.contains("band numbers must be 1-based"));
+
+ // Test out of range band number
+ let result = bands.band(2);
+ assert!(result.is_err());
+ let err = result.err().unwrap().to_string();
+ assert!(err.contains("is out of range"));
+
+ // Test valid band number should still work
+ let result = bands.band(1);
+ assert!(result.is_ok());
+ let band = result.unwrap();
+ assert_eq!(band.data().len(), 100);
+ }
+}
diff --git a/rust/sedona-schema/src/lib.rs b/rust/sedona-raster/src/lib.rs
similarity index 89%
copy from rust/sedona-schema/src/lib.rs
copy to rust/sedona-raster/src/lib.rs
index 107ea77..89a00ba 100644
--- a/rust/sedona-schema/src/lib.rs
+++ b/rust/sedona-raster/src/lib.rs
@@ -15,8 +15,6 @@
// specific language governing permissions and limitations
// under the License.
-pub mod crs;
-pub mod datatypes;
-pub mod extension_type;
-pub mod matchers;
-pub mod schema;
+pub mod array;
+pub mod builder;
+pub mod traits;
diff --git a/rust/sedona-raster/src/traits.rs b/rust/sedona-raster/src/traits.rs
new file mode 100644
index 0000000..07dcc8b
--- /dev/null
+++ b/rust/sedona-raster/src/traits.rs
@@ -0,0 +1,120 @@
+// 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.
+
+use arrow_schema::ArrowError;
+
+use sedona_schema::raster::{BandDataType, StorageType};
+
+/// Metadata for a raster
+#[derive(Debug, Clone)]
+pub struct RasterMetadata {
+ pub width: u64,
+ pub height: u64,
+ pub upperleft_x: f64,
+ pub upperleft_y: f64,
+ pub scale_x: f64,
+ pub scale_y: f64,
+ pub skew_x: f64,
+ pub skew_y: f64,
+}
+
+/// Metadata for a single band
+#[derive(Debug, Clone)]
+pub struct BandMetadata {
+ pub nodata_value: Option<Vec<u8>>,
+ pub storage_type: StorageType,
+ pub datatype: BandDataType,
+ /// URL for OutDb reference (only used when storage_type == OutDbRef)
+ pub outdb_url: Option<String>,
+ /// Band ID within the OutDb resource (only used when storage_type ==
OutDbRef)
+ pub outdb_band_id: Option<u32>,
+}
+
+/// Trait for accessing complete raster data
+pub trait RasterRef {
+ /// Raster metadata accessor
+ fn metadata(&self) -> &dyn MetadataRef;
+ /// CRS accessor
+ fn crs(&self) -> Option<&str>;
+ /// Bands accessor
+ fn bands(&self) -> &dyn BandsRef;
+}
+
+/// Trait for accessing raster metadata (dimensions, geotransform, bounding
box, etc.)
+pub trait MetadataRef {
+ /// Width of the raster in pixels
+ fn width(&self) -> u64;
+ /// Height of the raster in pixels
+ fn height(&self) -> u64;
+ /// X coordinate of the upper-left corner
+ fn upper_left_x(&self) -> f64;
+ /// Y coordinate of the upper-left corner
+ fn upper_left_y(&self) -> f64;
+ /// X-direction pixel size (scale)
+ fn scale_x(&self) -> f64;
+ /// Y-direction pixel size (scale)
+ fn scale_y(&self) -> f64;
+ /// X-direction skew/rotation
+ fn skew_x(&self) -> f64;
+ /// Y-direction skew/rotation
+ fn skew_y(&self) -> f64;
+}
+/// Trait for accessing all bands in a raster
+pub trait BandsRef {
+ /// Number of bands in the raster
+ fn len(&self) -> usize;
+ /// Check if no bands are present
+ fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+ /// Get a specific band by number (returns Error if out of bounds)
+ /// By convention, band numbers are 1-based
+ fn band(&self, number: usize) -> Result<Box<dyn BandRef + '_>, ArrowError>;
+ /// Iterator over all bands
+ fn iter(&self) -> Box<dyn BandIterator<'_> + '_>;
+}
+
+/// Trait for accessing individual band data
+pub trait BandRef {
+ /// Band metadata accessor
+ fn metadata(&self) -> &dyn BandMetadataRef;
+ /// Raw band data as bytes (zero-copy access)
+ fn data(&self) -> &[u8];
+}
+
+/// Trait for accessing individual band metadata
+pub trait BandMetadataRef {
+ /// No-data value as raw bytes (None if null)
+ fn nodata_value(&self) -> Option<&[u8]>;
+ /// Storage type (InDb, OutDbRef, etc)
+ fn storage_type(&self) -> StorageType;
+ /// Band data type (UInt8, Float32, etc.)
+ fn data_type(&self) -> BandDataType;
+ /// OutDb URL (only used when storage_type == OutDbRef)
+ fn outdb_url(&self) -> Option<&str>;
+ /// OutDb band ID (only used when storage_type == OutDbRef)
+ fn outdb_band_id(&self) -> Option<u32>;
+}
+
+/// Trait for iterating over bands within a raster
+pub trait BandIterator<'a>: Iterator<Item = Box<dyn BandRef + 'a>> {
+ fn len(&self) -> usize;
+ /// Check if there are no more bands
+ fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+}
diff --git a/rust/sedona-schema/src/datatypes.rs
b/rust/sedona-schema/src/datatypes.rs
index 254ca25..9c0d905 100644
--- a/rust/sedona-schema/src/datatypes.rs
+++ b/rust/sedona-schema/src/datatypes.rs
@@ -19,9 +19,11 @@ use datafusion_common::error::{DataFusionError, Result};
use sedona_common::sedona_internal_err;
use serde_json::Value;
use std::fmt::{Debug, Display};
+use std::sync::LazyLock;
use crate::crs::{deserialize_crs, Crs};
use crate::extension_type::ExtensionType;
+use crate::raster::RasterSchema;
/// Data types supported by Sedona that resolve to a concrete Arrow DataType
#[derive(Debug, PartialEq, Clone)]
@@ -29,6 +31,7 @@ pub enum SedonaType {
Arrow(DataType),
Wkb(Edges, Crs),
WkbView(Edges, Crs),
+ Raster,
}
impl From<DataType> for SedonaType {
@@ -72,6 +75,14 @@ pub const WKB_GEOGRAPHY: SedonaType =
SedonaType::Wkb(Edges::Spherical, Crs::Non
/// See [`WKB_GEOGRAPHY`]
pub const WKB_VIEW_GEOGRAPHY: SedonaType =
SedonaType::WkbView(Edges::Spherical, Crs::None);
+/// Sentinel for [`SedonaType::Raster`]
+pub const RASTER: SedonaType = SedonaType::Raster;
+
+/// Create a static value for the [`SedonaType::Raster`] that's initialized
exactly once,
+/// on first access
+static RASTER_DATATYPE: LazyLock<DataType> =
+ LazyLock::new(|| DataType::Struct(RasterSchema::fields()));
+
// Implementation details
impl SedonaType {
@@ -88,6 +99,15 @@ impl SedonaType {
let (edges, crs) =
deserialize_edges_and_crs(&extension.extension_metadata)?;
if extension.extension_name == "geoarrow.wkb" {
sedona_type_wkb(edges, crs, extension.storage_type)
+ } else if extension.extension_name == "sedona.raster" {
+ if extension.storage_type == *RASTER_DATATYPE {
+ Ok(RASTER)
+ } else {
+ sedona_internal_err!(
+ "Extension type sedona.raster has unexpected storage type:
{}",
+ extension.storage_type
+ )
+ }
} else {
sedona_internal_err!(
"Extension type not implemented: <{}>:{}",
@@ -111,6 +131,7 @@ impl SedonaType {
SedonaType::Arrow(data_type) => data_type,
SedonaType::Wkb(_, _) => &DataType::Binary,
SedonaType::WkbView(_, _) => &DataType::BinaryView,
+ SedonaType::Raster => &RASTER_DATATYPE,
}
}
@@ -119,6 +140,7 @@ impl SedonaType {
match self {
SedonaType::Arrow(_) => None,
SedonaType::Wkb(_, _) | SedonaType::WkbView(_, _) =>
Some("geoarrow.wkb"),
+ SedonaType::Raster => Some("sedona.raster"),
}
}
@@ -132,6 +154,11 @@ impl SedonaType {
Some(serialize_edges_and_crs(edges, crs)),
))
}
+ SedonaType::Raster => Some(ExtensionType::new(
+ self.extension_name().unwrap(),
+ self.storage_type().clone(),
+ None,
+ )),
_ => None,
}
}
@@ -150,6 +177,7 @@ impl SedonaType {
SedonaType::Wkb(Edges::Spherical, _) |
SedonaType::WkbView(Edges::Spherical, _) => {
"geography".to_string()
}
+ SedonaType::Raster => "raster".to_string(),
SedonaType::Arrow(data_type) => match data_type {
DataType::Utf8 | DataType::LargeUtf8 | DataType::Utf8View =>
"utf8".to_string(),
DataType::Binary
@@ -195,6 +223,7 @@ impl SedonaType {
(SedonaType::WkbView(edges, _), SedonaType::WkbView(other_edges,
_)) => {
edges == other_edges
}
+ (SedonaType::Raster, SedonaType::Raster) => true,
_ => false,
}
}
@@ -208,6 +237,7 @@ impl Display for SedonaType {
SedonaType::Arrow(data_type) => Display::fmt(data_type, f),
SedonaType::Wkb(edges, crs) => display_geometry("Wkb", edges, crs,
f),
SedonaType::WkbView(edges, crs) => display_geometry("WkbView",
edges, crs, f),
+ SedonaType::Raster => Display::fmt("Raster", f),
}
}
}
@@ -406,6 +436,7 @@ mod tests {
SedonaType::Wkb(Edges::Planar, projjson_crs).to_string(),
"Wkb({...})"
);
+ assert_eq!(RASTER.to_string(), "Raster");
}
#[test]
diff --git a/rust/sedona-schema/src/lib.rs b/rust/sedona-schema/src/lib.rs
index 107ea77..d0235e5 100644
--- a/rust/sedona-schema/src/lib.rs
+++ b/rust/sedona-schema/src/lib.rs
@@ -19,4 +19,5 @@ pub mod crs;
pub mod datatypes;
pub mod extension_type;
pub mod matchers;
+pub mod raster;
pub mod schema;
diff --git a/rust/sedona-schema/src/raster.rs b/rust/sedona-schema/src/raster.rs
new file mode 100644
index 0000000..1027e53
--- /dev/null
+++ b/rust/sedona-schema/src/raster.rs
@@ -0,0 +1,334 @@
+// 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.
+use arrow_schema::{DataType, Field, FieldRef, Fields};
+
+/// Schema for storing raster data in Apache Arrow format.
+/// Utilizing nested structs and lists to represent raster metadata and bands.
+#[derive(Debug, PartialEq, Clone)]
+pub struct RasterSchema;
+impl RasterSchema {
+ /// Returns the top-level fields for the raster schema structure.
+ pub fn fields() -> Fields {
+ Fields::from(vec![
+ Field::new(column::METADATA, Self::metadata_type(), false),
+ Field::new(column::CRS, Self::crs_type(), true), // Optional: may
be inferred from data
+ Field::new(column::BANDS, Self::bands_type(), true),
+ ])
+ }
+
+ /// Raster metadata schema
+ pub fn metadata_type() -> DataType {
+ DataType::Struct(Fields::from(vec![
+ // Raster dimensions
+ Field::new(column::WIDTH, DataType::UInt64, false),
+ Field::new(column::HEIGHT, DataType::UInt64, false),
+ // Geospatial transformation parameters
+ Field::new(column::UPPERLEFT_X, DataType::Float64, false),
+ Field::new(column::UPPERLEFT_Y, DataType::Float64, false),
+ Field::new(column::SCALE_X, DataType::Float64, false),
+ Field::new(column::SCALE_Y, DataType::Float64, false),
+ Field::new(column::SKEW_X, DataType::Float64, false),
+ Field::new(column::SKEW_Y, DataType::Float64, false),
+ ]))
+ }
+
+ /// Bands list schema
+ pub fn bands_type() -> DataType {
+ DataType::List(FieldRef::new(Field::new(
+ column::BAND,
+ Self::band_type(),
+ false,
+ )))
+ }
+
+ /// Individual band schema
+ pub fn band_type() -> DataType {
+ DataType::Struct(Fields::from(vec![
+ Field::new(column::METADATA, Self::band_metadata_type(), false),
+ Field::new(column::DATA, Self::band_data_type(), false),
+ ]))
+ }
+
+ /// Band metadata schema
+ pub fn band_metadata_type() -> DataType {
+ DataType::Struct(Fields::from(vec![
+ Field::new(column::NODATAVALUE, DataType::Binary, true), //
Optional: null means no nodata value specified
+ Field::new(column::STORAGE_TYPE, DataType::UInt32, false),
+ Field::new(column::DATATYPE, DataType::UInt32, false),
+ // OutDb reference fields - only used when storage_type == OutDbRef
+ Field::new(column::OUTDB_URL, DataType::Utf8, true),
+ Field::new(column::OUTDB_BAND_ID, DataType::UInt32, true),
+ ]))
+ }
+
+ /// Band data schema - stores the actual raster pixel data as a binary blob
+ pub fn band_data_type() -> DataType {
+ DataType::BinaryView
+ }
+
+ /// Coordinate Reference System (CRS) schema - stores CRS as JSON string
(PROJ or WKT format)
+ pub fn crs_type() -> DataType {
+ DataType::Utf8View
+ }
+}
+
+/// Band data type enumeration for raster bands.
+///
+/// Only supports basic numeric types.
+/// In future versions, consider support for complex types used in
+/// radar and other wave-based data.
+#[repr(u16)]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum BandDataType {
+ UInt8 = 0,
+ UInt16 = 1,
+ Int16 = 2,
+ UInt32 = 3,
+ Int32 = 4,
+ Float32 = 5,
+ Float64 = 6,
+}
+
+/// Storage strategy for raster band data within Apache Arrow arrays.
+///
+/// This enum defines how raster data is physically stored and accessed:
+///
+/// **InDb**: Raster data is embedded directly in the Arrow array as binary
blobs.
+/// - Self-contained, no external dependencies, fast access for small-medium
rasters
+/// - Increases Arrow array size, memory usage grows and copy times increase
with raster size
+/// - Best for: Tiles, thumbnails, processed results, small rasters (<10MB
per band)
+///
+/// **OutDbRef**: Raster data is stored externally with references in the
Arrow array.
+/// - Keeps Arrow arrays lightweight, supports massive rasters, enables lazy
loading
+/// - Requires external storage management, potential for broken references
+/// - Best for: Large satellite imagery, time series data, cloud-native
workflows
+/// - Supported backends: S3, GCS, Azure Blob, local filesystem, HTTP
endpoints
+#[repr(u16)]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum StorageType {
+ InDb = 0,
+ OutDbRef = 1,
+}
+
+/// Hard-coded column indices for performant access to nested struct fields.
+/// These indices must match the exact order defined in the RasterSchema
methods.
+///
+/// Using compile-time constants avoids string lookups and provides type safety
+/// when accessing nested struct fields in Arrow arrays.
+pub mod metadata_indices {
+ pub const WIDTH: usize = 0;
+ pub const HEIGHT: usize = 1;
+ pub const UPPERLEFT_X: usize = 2;
+ pub const UPPERLEFT_Y: usize = 3;
+ pub const SCALE_X: usize = 4;
+ pub const SCALE_Y: usize = 5;
+ pub const SKEW_X: usize = 6;
+ pub const SKEW_Y: usize = 7;
+}
+
+pub mod band_metadata_indices {
+ pub const NODATAVALUE: usize = 0;
+ pub const STORAGE_TYPE: usize = 1;
+ pub const DATATYPE: usize = 2;
+ pub const OUTDB_URL: usize = 3;
+ pub const OUTDB_BAND_ID: usize = 4;
+}
+
+pub mod band_indices {
+ pub const METADATA: usize = 0;
+ pub const DATA: usize = 1;
+}
+
+pub mod raster_indices {
+ pub const METADATA: usize = 0;
+ pub const CRS: usize = 1;
+ pub const BANDS: usize = 2;
+}
+
+/// Column name constants used throughout the raster schema definition.
+/// These string constants ensure consistency across schema creation and field
access.
+pub mod column {
+ pub const METADATA: &str = "metadata";
+ pub const BANDS: &str = "bands";
+ pub const BAND: &str = "band";
+ pub const DATA: &str = "data";
+
+ // Raster metadata fields
+ pub const WIDTH: &str = "width";
+ pub const HEIGHT: &str = "height";
+ pub const UPPERLEFT_X: &str = "upperleft_x";
+ pub const UPPERLEFT_Y: &str = "upperleft_y";
+ pub const SCALE_X: &str = "scale_x";
+ pub const SCALE_Y: &str = "scale_y";
+ pub const SKEW_X: &str = "skew_x";
+ pub const SKEW_Y: &str = "skew_y";
+
+ // Raster CRS field
+ pub const CRS: &str = "crs";
+ // Band metadata fields
+ pub const NODATAVALUE: &str = "nodata_value";
+ pub const STORAGE_TYPE: &str = "storage_type";
+ pub const DATATYPE: &str = "data_type";
+ pub const OUTDB_URL: &str = "outdb_url";
+ pub const OUTDB_BAND_ID: &str = "outdb_band_id";
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ /// Tests that the top-level raster schema has the expected number and
names of fields.
+ #[test]
+ fn test_raster_schema_fields() {
+ let fields = RasterSchema::fields();
+ assert_eq!(fields.len(), 3);
+ assert_eq!(fields[0].name(), column::METADATA);
+ assert_eq!(fields[1].name(), column::CRS);
+ assert_eq!(fields[2].name(), column::BANDS);
+ }
+
+ /// Comprehensive test to verify all hard-coded indices match the actual
schema.
+ /// This ensures that performance optimizations using direct index access
remain valid
+ /// when the schema structure changes.
+ #[test]
+ fn test_hardcoded_indices_match_schema() {
+ // Test raster-level indices
+ let raster_fields = RasterSchema::fields();
+ assert_eq!(raster_fields.len(), 3, "Expected exactly 3 raster fields");
+ assert_eq!(
+ raster_fields[raster_indices::METADATA].name(),
+ column::METADATA,
+ "Raster metadata index mismatch"
+ );
+ assert_eq!(
+ raster_fields[raster_indices::CRS].name(),
+ column::CRS,
+ "Raster CRS index mismatch"
+ );
+ assert_eq!(
+ raster_fields[raster_indices::BANDS].name(),
+ column::BANDS,
+ "Raster BANDS index mismatch"
+ );
+
+ // Test metadata indices
+ let metadata_type = RasterSchema::metadata_type();
+ if let DataType::Struct(metadata_fields) = metadata_type {
+ assert_eq!(
+ metadata_fields.len(),
+ 8,
+ "Expected exactly 8 metadata fields"
+ );
+ assert_eq!(
+ metadata_fields[metadata_indices::WIDTH].name(),
+ column::WIDTH,
+ "Metadata width index mismatch"
+ );
+ assert_eq!(
+ metadata_fields[metadata_indices::HEIGHT].name(),
+ column::HEIGHT,
+ "Metadata height index mismatch"
+ );
+ assert_eq!(
+ metadata_fields[metadata_indices::UPPERLEFT_X].name(),
+ column::UPPERLEFT_X,
+ "Metadata upperleft_x index mismatch"
+ );
+ assert_eq!(
+ metadata_fields[metadata_indices::UPPERLEFT_Y].name(),
+ column::UPPERLEFT_Y,
+ "Metadata upperleft_y index mismatch"
+ );
+ assert_eq!(
+ metadata_fields[metadata_indices::SCALE_X].name(),
+ column::SCALE_X,
+ "Metadata scale_x index mismatch"
+ );
+ assert_eq!(
+ metadata_fields[metadata_indices::SCALE_Y].name(),
+ column::SCALE_Y,
+ "Metadata scale_y index mismatch"
+ );
+ assert_eq!(
+ metadata_fields[metadata_indices::SKEW_X].name(),
+ column::SKEW_X,
+ "Metadata skew_x index mismatch"
+ );
+ assert_eq!(
+ metadata_fields[metadata_indices::SKEW_Y].name(),
+ column::SKEW_Y,
+ "Metadata skew_y index mismatch"
+ );
+ } else {
+ panic!("Expected Struct type for metadata");
+ }
+
+ // Test band metadata indices
+ let band_metadata_type = RasterSchema::band_metadata_type();
+ if let DataType::Struct(band_metadata_fields) = band_metadata_type {
+ assert_eq!(
+ band_metadata_fields.len(),
+ 5,
+ "Expected exactly 5 band metadata fields"
+ );
+ assert_eq!(
+
band_metadata_fields[band_metadata_indices::NODATAVALUE].name(),
+ column::NODATAVALUE,
+ "Band metadata nodatavalue index mismatch"
+ );
+ assert_eq!(
+
band_metadata_fields[band_metadata_indices::STORAGE_TYPE].name(),
+ column::STORAGE_TYPE,
+ "Band metadata storage_type index mismatch"
+ );
+ assert_eq!(
+ band_metadata_fields[band_metadata_indices::DATATYPE].name(),
+ column::DATATYPE,
+ "Band metadata datatype index mismatch"
+ );
+ assert_eq!(
+ band_metadata_fields[band_metadata_indices::OUTDB_URL].name(),
+ column::OUTDB_URL,
+ "Band metadata outdb_url index mismatch"
+ );
+ assert_eq!(
+
band_metadata_fields[band_metadata_indices::OUTDB_BAND_ID].name(),
+ column::OUTDB_BAND_ID,
+ "Band metadata outdb_band_id index mismatch"
+ );
+ } else {
+ panic!("Expected Struct type for band metadata");
+ }
+
+ // Test band indices
+ let band_type = RasterSchema::band_type();
+ if let DataType::Struct(band_fields) = band_type {
+ assert_eq!(band_fields.len(), 2, "Expected exactly 2 band fields");
+ assert_eq!(
+ band_fields[band_indices::METADATA].name(),
+ column::METADATA,
+ "Band metadata index mismatch"
+ );
+ assert_eq!(
+ band_fields[band_indices::DATA].name(),
+ column::DATA,
+ "Band data index mismatch"
+ );
+ } else {
+ panic!("Expected Struct type for band");
+ }
+ }
+}