Skip to content

Commit 97ef0f4

Browse files
authored
feat(rust/sedona-raster): BandRef::copy_into derive-with-overrides (zero-copy) (#964)
1 parent ce74a89 commit 97ef0f4

3 files changed

Lines changed: 340 additions & 2 deletions

File tree

rust/sedona-raster/src/array.rs

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use datafusion_common::cast::{
2525
as_string_array, as_string_view_array, as_struct_array, as_uint32_array,
2626
};
2727

28+
use crate::builder::RasterBuilder;
2829
use crate::traits::{BandRef, Bands, NdBuffer, RasterRef};
2930
use crate::view_entries::ViewEntry;
3031
use sedona_schema::raster::{band_indices, raster_indices, BandDataType};
@@ -138,6 +139,18 @@ impl<'a> BandRef for BandRefImpl<'a> {
138139
data_type: self.data_type,
139140
})
140141
}
142+
143+
/// Zero-copy override: share the source row's backing `Buffer` into the
144+
/// builder (refcount bump) instead of copying the visible bytes. OutDb
145+
/// bands have an empty data column by design.
146+
fn append_data_into(&self, builder: &mut RasterBuilder) -> Result<(), ArrowError> {
147+
if self.is_indb() {
148+
builder.append_band_data_from(self.data_array, self.band_row)
149+
} else {
150+
builder.band_data_writer().append_value([]);
151+
Ok(())
152+
}
153+
}
141154
}
142155

143156
/// Arrow-backed implementation of RasterRef for a single raster row.
@@ -545,7 +558,7 @@ impl<'a> RasterStructArray<'a> {
545558
mod tests {
546559
use super::*;
547560
use crate::builder::RasterBuilder;
548-
use crate::traits::{BandMetadata, RasterMetadata};
561+
use crate::traits::{BandMetadata, BandOverrides, RasterMetadata};
549562
use arrow_array::{ArrayRef, ListArray, StructArray, UInt32Array};
550563
use arrow_buffer::{OffsetBuffer, ScalarBuffer};
551564
use arrow_schema::{DataType, Fields};
@@ -555,6 +568,122 @@ mod tests {
555568
use sedona_testing::rasters::generate_test_rasters;
556569
use std::sync::Arc;
557570

571+
#[test]
572+
fn copy_into_shares_buffer_zero_copy_and_overrides() {
573+
// 16-byte InDb band (> inline threshold, so block-backed and shareable).
574+
let transform = [0.0, 1.0, 0.0, 0.0, 0.0, -1.0];
575+
let mut ib = RasterBuilder::new(1);
576+
ib.start_raster_nd(&transform, &["x"], &[16], None).unwrap();
577+
ib.start_band_nd(
578+
Some("orig"),
579+
&["x"],
580+
&[16],
581+
BandDataType::UInt8,
582+
None,
583+
None,
584+
None,
585+
)
586+
.unwrap();
587+
ib.band_data_writer()
588+
.append_value((0u8..16).collect::<Vec<u8>>());
589+
ib.finish_band().unwrap();
590+
ib.finish_raster().unwrap();
591+
let input_array = ib.finish().unwrap();
592+
let input_rasters = RasterStructArray::try_new(&input_array).unwrap();
593+
let input_raster = input_rasters.get(0).unwrap();
594+
let input_band = input_raster.band(0).unwrap();
595+
let input_ptr = input_band.nd_buffer().unwrap().buffer.as_ptr();
596+
597+
// copy_into with a name override; everything else inherited.
598+
let mut ob = RasterBuilder::new(1);
599+
ob.start_raster_nd(&transform, &["x"], &[16], None).unwrap();
600+
input_band
601+
.copy_into(
602+
&mut ob,
603+
BandOverrides {
604+
name: Some("derived"),
605+
..Default::default()
606+
},
607+
)
608+
.unwrap();
609+
ob.finish_band().unwrap();
610+
ob.finish_raster().unwrap();
611+
let out_array = ob.finish().unwrap();
612+
let out_rasters = RasterStructArray::try_new(&out_array).unwrap();
613+
let out_raster = out_rasters.get(0).unwrap();
614+
let out_band = out_raster.band(0).unwrap();
615+
616+
// Zero-copy: the derived band references the same backing bytes.
617+
assert_eq!(
618+
input_ptr,
619+
out_band.nd_buffer().unwrap().buffer.as_ptr(),
620+
"copy_into must share the source buffer, not copy it"
621+
);
622+
assert_eq!(
623+
out_band.nd_buffer().unwrap().as_contiguous().unwrap(),
624+
(0u8..16).collect::<Vec<u8>>().as_slice()
625+
);
626+
// Name overridden; dim names + data type inherited from the source.
627+
assert_eq!(out_raster.band_name(0), Some("derived"));
628+
assert_eq!(out_band.dim_names(), vec!["x"]);
629+
assert_eq!(out_band.data_type(), BandDataType::UInt8);
630+
}
631+
632+
#[test]
633+
fn copy_into_with_identity_override_view_succeeds() {
634+
// An explicit identity override composes back to the identity, so it is
635+
// accepted and behaves exactly like the inherited (None) case — this
636+
// exercises the new `BandOverrides::view` path end to end.
637+
let transform = [0.0, 1.0, 0.0, 0.0, 0.0, -1.0];
638+
let mut ib = RasterBuilder::new(1);
639+
ib.start_raster_nd(&transform, &["x"], &[4], None).unwrap();
640+
ib.start_band_nd(
641+
Some("orig"),
642+
&["x"],
643+
&[4],
644+
BandDataType::UInt8,
645+
None,
646+
None,
647+
None,
648+
)
649+
.unwrap();
650+
ib.band_data_writer().append_value(vec![1u8, 2, 3, 4]);
651+
ib.finish_band().unwrap();
652+
ib.finish_raster().unwrap();
653+
let in_array = ib.finish().unwrap();
654+
let in_rasters = RasterStructArray::try_new(&in_array).unwrap();
655+
let in_raster = in_rasters.get(0).unwrap();
656+
let in_band = in_raster.band(0).unwrap();
657+
658+
let identity = [ViewEntry {
659+
source_axis: 0,
660+
start: 0,
661+
step: 1,
662+
steps: 4,
663+
}];
664+
let mut ob = RasterBuilder::new(1);
665+
ob.start_raster_nd(&transform, &["x"], &[4], None).unwrap();
666+
in_band
667+
.copy_into(
668+
&mut ob,
669+
BandOverrides {
670+
view: Some(&identity),
671+
..Default::default()
672+
},
673+
)
674+
.unwrap();
675+
ob.finish_band().unwrap();
676+
ob.finish_raster().unwrap();
677+
let out_array = ob.finish().unwrap();
678+
let out_rasters = RasterStructArray::try_new(&out_array).unwrap();
679+
let out_raster = out_rasters.get(0).unwrap();
680+
let out_band = out_raster.band(0).unwrap();
681+
assert_eq!(
682+
out_band.nd_buffer().unwrap().as_contiguous().unwrap(),
683+
&[1u8, 2, 3, 4]
684+
);
685+
}
686+
558687
#[test]
559688
fn test_array_basic_functionality() {
560689
// Create a simple raster for testing using the correct API

rust/sedona-raster/src/builder.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use std::sync::Arc;
3030
use sedona_schema::raster::{BandDataType, RasterSchema};
3131

3232
use crate::traits::{BandMetadata, MetadataRef};
33+
use crate::view_entries::{ViewEntries, ViewEntry};
3334

3435
/// Maximum byte length of an inline `BinaryViewArray` view. Views this short
3536
/// store their bytes in the 16-byte view itself; longer views reference a data
@@ -144,6 +145,20 @@ pub struct RasterBuilder {
144145
raster_validity: BooleanBuilder,
145146
}
146147

148+
/// Arguments to [`RasterBuilder::start_band_with_view`]. Bundled into a
149+
/// struct to keep the call site readable — eight slots is enough that
150+
/// positional args invite mis-ordering bugs.
151+
pub(crate) struct StartBandWithViewArgs<'a> {
152+
pub name: Option<&'a str>,
153+
pub dim_names: &'a [&'a str],
154+
pub source_shape: &'a [i64],
155+
pub view: &'a [ViewEntry],
156+
pub data_type: BandDataType,
157+
pub nodata: Option<&'a [u8]>,
158+
pub outdb_uri: Option<&'a str>,
159+
pub outdb_format: Option<&'a str>,
160+
}
161+
147162
impl RasterBuilder {
148163
/// Create a new raster builder with the specified capacity.
149164
pub fn new(capacity: usize) -> Self {
@@ -387,6 +402,69 @@ impl RasterBuilder {
387402
Ok(())
388403
}
389404

405+
/// Start a band carrying an explicit `view` — a per-axis window of
406+
/// offsets/steps over `source_shape` — rather than the implicit identity.
407+
///
408+
/// This is the entry point view persistence will use, so callers such as
409+
/// [`BandRef::copy_into`] route through it unchanged once persistence
410+
/// lands. Today the band schema stores a view only as the canonical
411+
/// identity null sentinel, so a non-identity `view` is rejected; an
412+
/// identity view is stored exactly as [`Self::start_band_nd`] does. View
413+
/// persistence is tracked in
414+
/// <https://github.com/apache/sedona-db/issues/897>.
415+
pub(crate) fn start_band_with_view(
416+
&mut self,
417+
args: StartBandWithViewArgs<'_>,
418+
) -> Result<(), ArrowError> {
419+
let StartBandWithViewArgs {
420+
name,
421+
dim_names,
422+
source_shape,
423+
view,
424+
data_type,
425+
nodata,
426+
outdb_uri,
427+
outdb_format,
428+
} = args;
429+
let ndim = dim_names.len();
430+
if ndim == 0 {
431+
return Err(ArrowError::InvalidArgumentError(
432+
"start_band_with_view: 0-dimensional bands are not supported".into(),
433+
));
434+
}
435+
if source_shape.len() != ndim || view.len() != ndim {
436+
return Err(ArrowError::InvalidArgumentError(format!(
437+
"start_band_with_view: dim_names ({}), source_shape ({}), and view ({}) \
438+
must all have the same length",
439+
ndim,
440+
source_shape.len(),
441+
view.len()
442+
)));
443+
}
444+
let view_entries = ViewEntries::new(view.to_vec());
445+
view_entries.validate(source_shape)?;
446+
// The schema stores views only as the identity null sentinel today, so a
447+
// non-identity view can't round-trip — reject it up front, before any
448+
// column append, rather than persisting mislocated bytes.
449+
if !view_entries.is_identity(source_shape) {
450+
return Err(ArrowError::InvalidArgumentError(
451+
"start_band_with_view: persisting a non-identity band view is not yet \
452+
supported (see https://github.com/apache/sedona-db/issues/897); \
453+
materialize the band (e.g. via RS_EnsureContiguous) first"
454+
.into(),
455+
));
456+
}
457+
self.start_band_nd(
458+
name,
459+
dim_names,
460+
source_shape,
461+
data_type,
462+
nodata,
463+
outdb_uri,
464+
outdb_format,
465+
)
466+
}
467+
390468
/// Convenience: start a 2D band with `dim_names=["y","x"]` and `shape=[height, width]`.
391469
///
392470
/// Must be called after `start_raster_2d` / `start_raster_2d` which sets

0 commit comments

Comments
 (0)