Skip to content

Commit 9267b04

Browse files
committed
refactor(raster): use i64 throughout the view-machinery surface
Per Dewey's review: every `source_axis`/`source_shape`/`visible_shape` in the view helpers should be `i64` to match `ViewEntry`'s signed fields and the stride arithmetic, instead of round-tripping through `u64` and sprinkling `as` casts through helper bodies. Reworked to make i64 the canonical view-machinery type — no more u64↔i64 conversions in `data()`, `nd_buffer()`, `contiguous_data()`, or the band-construction storage step. Public surface changes: - `BandRef::shape() -> &[i64]` (was `&[u64]`). Production callers outside the raster crate's own tests don't exist; tests use array literals which infer the new element type. `width()` and `height()` stay `u64` (conventional non-negative scalar counts) and pick up a single `as u64` cast in the default `dim_size()` impl. - `BandRef::raw_source_shape() -> &[u64]` unchanged — this is a direct slice into the Arrow `UInt64Array` column, changing the return type would force an alloc per call. - `NdBuffer.shape: Vec<i64>`, `NdBuffer.offset: i64` — internal struct, no out-of-crate consumers. Internals now `i64`: - `validate_view(view, source_shape: &[i64])`, with an added `source_shape[sa] >= 0` check at the boundary. - `is_identity_view(view, source_shape: &[i64])`. - `visible_shape_from_view(view) -> Vec<i64>`. - `BandRefImpl::visible_shape: Vec<i64>` (matches the trait return). - `materialize_strided` and `check_view_buffer_bounds` take `visible_shape: &[i64]`. Conversions concentrated at the two unavoidable boundaries — both are Arrow column reads: - `RasterRefImpl::band()` converts `raw_source_shape() -> &[u64]` into `Vec<i64>` once with a `try_from` overflow check. - `RasterBuilder::start_band_with_view` does the same conversion at its `source_shape: &[u64]` entry point before calling `validate_view`. Eliminates the source-stride `i64::try_from` cast that used to live inside `band()` (line ~527 pre-refactor). Tests pass: sedona-raster 131, sedona-raster-functions 143, sedona-raster-gdal 53. Arrow column storage and the public `width()`/`height()` API unchanged.
1 parent e890f90 commit 9267b04

3 files changed

Lines changed: 103 additions & 54 deletions

File tree

rust/sedona-raster/src/array.rs

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,11 @@ struct BandRefImpl<'a> {
5050
data_type: BandDataType,
5151
/// Per-visible-axis view, length = ndim
5252
view_entries: Vec<ViewEntry>,
53-
/// Visible shape (== `[v.steps for v in view_entries]`), length = ndim
54-
visible_shape: Vec<u64>,
53+
/// Visible shape (== `[v.steps for v in view_entries]`), length = ndim.
54+
/// `i64` to match `BandRef::shape()`'s return type and the surrounding
55+
/// view-machinery arithmetic (strides, offsets). `validate_view`
56+
/// guarantees entries are non-negative.
57+
visible_shape: Vec<i64>,
5558
/// Byte strides per visible axis. May be 0 (broadcast) or negative.
5659
byte_strides: Vec<i64>,
5760
/// Byte offset into `data` of the visible region's `[0,...,0]` element.
@@ -83,7 +86,7 @@ impl<'a> BandRef for BandRefImpl<'a> {
8386
.collect()
8487
}
8588

86-
fn shape(&self) -> &[u64] {
89+
fn shape(&self) -> &[i64] {
8790
&self.visible_shape
8891
}
8992

@@ -168,13 +171,11 @@ impl<'a> BandRef for BandRefImpl<'a> {
168171
}
169172
// shape and strides are owned by NdBuffer (see its doc comment).
170173
// Cloning here is cheap — both vecs are O(ndim), a handful of values.
171-
// Cast offset i64 -> u64: safe because RasterRefImpl::band asserts
172-
// byte_offset >= 0 before storing.
173174
Ok(NdBuffer {
174175
buffer: self.data_array.value(self.band_row),
175176
shape: self.visible_shape.clone(),
176177
strides: self.byte_strides.clone(),
177-
offset: self.byte_offset as u64,
178+
offset: self.byte_offset,
178179
data_type: self.data_type,
179180
})
180181
}
@@ -186,10 +187,7 @@ impl<'a> BandRef for BandRefImpl<'a> {
186187
// ARE the visible bytes. Borrow them.
187188
return Ok(Cow::Borrowed(buf.buffer));
188189
}
189-
// Use self.* layout fields rather than `buf.*` to avoid the
190-
// i64 -> u64 -> i64 round-trip through NdBuffer for `byte_offset`.
191-
// The visible shape and byte strides are precomputed once at
192-
// construction; `buf.shape` / `buf.strides` are clones of those.
190+
// `buf.shape` is a clone of self.visible_shape; use self.* directly.
193191
let out = materialize_strided(
194192
buf.buffer,
195193
&self.visible_shape,
@@ -220,7 +218,7 @@ impl<'a> BandRef for BandRefImpl<'a> {
220218
/// and skip the check.
221219
fn check_view_buffer_bounds(
222220
buffer_len: usize,
223-
visible_shape: &[u64],
221+
visible_shape: &[i64],
224222
byte_strides: &[i64],
225223
byte_offset: i64,
226224
dtype_size: usize,
@@ -231,9 +229,9 @@ fn check_view_buffer_bounds(
231229
let mut min_offset = byte_offset;
232230
let mut max_offset = byte_offset;
233231
for (k, &stride) in byte_strides.iter().enumerate() {
234-
let last_idx = i64::try_from(visible_shape[k] - 1).map_err(|_| {
235-
ArrowError::InvalidArgumentError(format!("visible_shape[{k}] - 1 exceeds i64::MAX"))
236-
})?;
232+
// `validate_view` guarantees `steps >= 0`, so `visible_shape[k] >= 0`
233+
// and `visible_shape[k] - 1` is in-range for any non-empty axis.
234+
let last_idx = visible_shape[k] - 1;
237235
let contribution = last_idx.checked_mul(stride).ok_or_else(|| {
238236
ArrowError::InvalidArgumentError(format!(
239237
"max addressable offset on axis {k} overflows i64"
@@ -285,7 +283,7 @@ fn check_view_buffer_bounds(
285283
/// surface the failure.
286284
fn materialize_strided(
287285
buffer: &[u8],
288-
visible_shape: &[u64],
286+
visible_shape: &[i64],
289287
byte_strides: &[i64],
290288
byte_offset: i64,
291289
dtype_size: usize,
@@ -302,7 +300,7 @@ fn materialize_strided(
302300
// we'd otherwise repeat here, so this walk uses plain arithmetic and
303301
// unchecked slice indexing within the buffer.
304302
let ndim = visible_shape.len();
305-
let total: u64 = visible_shape.iter().product();
303+
let total: i64 = visible_shape.iter().product();
306304
if total == 0 {
307305
return Vec::new();
308306
}
@@ -322,13 +320,13 @@ fn materialize_strided(
322320

323321
// Precompute a small index vector for outer axes (everything except
324322
// the innermost). For 1D this is empty and we run a single pass.
325-
let mut outer_idx = vec![0u64; ndim.saturating_sub(1)];
323+
let mut outer_idx = vec![0i64; ndim.saturating_sub(1)];
326324
loop {
327325
// Compute the byte offset of the row's first element from the
328326
// current outer index combination.
329327
let mut row_off = base;
330328
for (k, &i) in outer_idx.iter().enumerate() {
331-
row_off += (i as i64) * byte_strides[k];
329+
row_off += i * byte_strides[k];
332330
}
333331

334332
if row_bytes_contiguous {
@@ -435,18 +433,37 @@ impl<'a> RasterRef for RasterRefImpl<'a> {
435433
// Read source shape slice.
436434
let ss_start = self.band_source_shape_list.value_offsets()[band_row] as usize;
437435
let ss_end = self.band_source_shape_list.value_offsets()[band_row + 1] as usize;
438-
let source_shape: &[u64] = &self.band_source_shape_values.values()[ss_start..ss_end];
436+
let source_shape_u64: &[u64] = &self.band_source_shape_values.values()[ss_start..ss_end];
439437

440438
// Reject 0-D bands at the read boundary. Schema doesn't forbid them
441439
// outright but every consumer assumes ndim >= 1.
442-
if source_shape.is_empty() {
440+
if source_shape_u64.is_empty() {
443441
return Err(ArrowError::ExternalError(Box::new(
444442
sedona_common::sedona_internal_datafusion_err!(
445443
"band {band_row} has empty source_shape; ndim must be >= 1"
446444
),
447445
)));
448446
}
449447

448+
// Convert source_shape u64 → i64 once with overflow check. Every
449+
// downstream consumer in the view machinery wants i64 (matches
450+
// ViewEntry's signed fields and the stride arithmetic). The cast
451+
// is fallible only on cosmically large dims (> 2^63); a clean
452+
// internal error is better than a wrap that silently passes
453+
// later bound checks.
454+
let source_shape: Vec<i64> = source_shape_u64
455+
.iter()
456+
.map(|&s| {
457+
i64::try_from(s).map_err(|_| {
458+
ArrowError::ExternalError(Box::new(
459+
sedona_common::sedona_internal_datafusion_err!(
460+
"band {band_row}: source_shape axis {s} exceeds i64::MAX"
461+
),
462+
))
463+
})
464+
})
465+
.collect::<Result<_, _>>()?;
466+
450467
// Resolve data type up front; an unknown discriminant is a
451468
// schema-corruption bug, not user data, so failing the band loudly
452469
// here is appropriate.
@@ -474,7 +491,7 @@ impl<'a> RasterRef for RasterRefImpl<'a> {
474491
source_axis: i as i64,
475492
start: 0,
476493
step: 1,
477-
steps: s as i64,
494+
steps: s,
478495
})
479496
.collect()
480497
} else {
@@ -492,7 +509,7 @@ impl<'a> RasterRef for RasterRefImpl<'a> {
492509

493510
// Full validation: length match, source_axis permutation, bounds,
494511
// and steps >= 0. Anything malformed is schema-level corruption.
495-
if let Err(e) = validate_view(&view_entries, source_shape) {
512+
if let Err(e) = validate_view(&view_entries, &source_shape) {
496513
return Err(ArrowError::ExternalError(Box::new(
497514
sedona_common::sedona_internal_datafusion_err!(
498515
"band {band_row} has malformed view: {e}"
@@ -517,17 +534,14 @@ impl<'a> RasterRef for RasterRefImpl<'a> {
517534
// C-order byte strides over the source_shape:
518535
// source_strides_bytes[k] = dtype_size * Π_{j>k} source_shape[j]
519536
//
520-
// Computed with checked arithmetic so a corrupt source_shape (a u64
521-
// that doesn't fit in i64, or a product that overflows) is rejected
522-
// here rather than producing a wrapped stride that silently passes
523-
// later bound checks.
537+
// Computed with checked arithmetic so a corrupt source_shape whose
538+
// product overflows i64 is rejected rather than producing a wrapped
539+
// stride that silently passes later bound checks.
524540
let mut source_strides_bytes = vec![0i64; source_shape.len()];
525541
source_strides_bytes[source_shape.len() - 1] = dtype_size;
526542
for k in (0..source_shape.len() - 1).rev() {
527-
let next_axis = i64::try_from(source_shape[k + 1])
528-
.map_err(|_| overflow_err("source_shape axis exceeds i64::MAX"))?;
529543
source_strides_bytes[k] = source_strides_bytes[k + 1]
530-
.checked_mul(next_axis)
544+
.checked_mul(source_shape[k + 1])
531545
.ok_or_else(|| overflow_err("source-stride product overflows i64"))?;
532546
}
533547

@@ -552,7 +566,7 @@ impl<'a> RasterRef for RasterRefImpl<'a> {
552566
.checked_add(start_off)
553567
.ok_or_else(|| overflow_err("view offset accumulation overflows i64"))?;
554568
}
555-
let is_identity_view = is_identity_view(&view_entries, source_shape);
569+
let is_identity_view = is_identity_view(&view_entries, &source_shape);
556570
// byte_offset is non-negative by construction (start >= 0,
557571
// src_stride > 0). Check defensively so a future refactor that
558572
// breaks the invariant surfaces a clean internal error rather than

rust/sedona-raster/src/builder.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,21 @@ impl RasterBuilder {
419419
)));
420420
}
421421

422-
validate_view(view, source_shape)?;
422+
// Convert source_shape u64 → i64 for validation. `start_band_with_view`
423+
// is a builder entry point so producers can express a source_shape
424+
// that doesn't fit in i64 — reject it here rather than letting it
425+
// wrap inside `validate_view`.
426+
let source_shape_i64: Vec<i64> = source_shape
427+
.iter()
428+
.map(|&s| {
429+
i64::try_from(s).map_err(|_| {
430+
ArrowError::InvalidArgumentError(format!(
431+
"start_band_with_view: source_shape axis {s} exceeds i64::MAX"
432+
))
433+
})
434+
})
435+
.collect::<Result<_, _>>()?;
436+
validate_view(view, &source_shape_i64)?;
423437

424438
// Write fields.
425439
match name {
@@ -470,9 +484,16 @@ impl RasterBuilder {
470484
self.band_data_count_at_start = self.band_data.len();
471485

472486
// finish_raster compares visible shape against spatial_shape.
487+
// `visible_shape_from_view` returns Vec<i64>; the entries are
488+
// guaranteed non-negative by `validate_view` above, so the cast
489+
// to u64 is lossless.
490+
let visible_shape_u64: Vec<u64> = visible_shape_from_view(view)
491+
.into_iter()
492+
.map(|v| v as u64)
493+
.collect();
473494
self.current_raster_bands.push((
474495
dim_names.iter().map(|s| s.to_string()).collect(),
475-
visible_shape_from_view(view),
496+
visible_shape_u64,
476497
));
477498

478499
Ok(())

rust/sedona-raster/src/traits.rs

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ use sedona_schema::raster::BandDataType;
3030
/// C-order. Consumers that need a flat row-major buffer should use
3131
/// `BandRef::contiguous_data()` instead.
3232
///
33+
/// `shape` and `offset` are `i64` (not `u64`) to match the surrounding
34+
/// stride arithmetic (`strides: Vec<i64>` to allow negative steps);
35+
/// shape elements are always non-negative — `validate_view` enforces
36+
/// this — and `offset` is non-negative by construction.
37+
///
3338
/// Only `buffer` is tied to the producer's lifetime `'a` (it can be tens of
3439
/// MBs of pixel data and must not be copied). `shape` and `strides` are
3540
/// owned `Vec`s — they're tiny (ndim ≤ a handful) so an allocation here is
@@ -38,9 +43,9 @@ use sedona_schema::raster::BandDataType;
3843
#[derive(Debug)]
3944
pub struct NdBuffer<'a> {
4045
pub buffer: &'a [u8],
41-
pub shape: Vec<u64>,
46+
pub shape: Vec<i64>,
4247
pub strides: Vec<i64>,
43-
pub offset: u64,
48+
pub offset: i64,
4449
pub data_type: BandDataType,
4550
}
4651

@@ -476,7 +481,11 @@ pub trait BandRef {
476481
/// `dim_names` order. Derived from `view`: `[v.steps for v in view]`.
477482
/// This is what almost all consumers want; use `raw_source_shape()` only
478483
/// when you need to address into the raw `data` buffer (e.g. FFI).
479-
fn shape(&self) -> &[u64];
484+
///
485+
/// `i64` (not `u64`) to match the surrounding view-machinery
486+
/// arithmetic (strides, offsets); shape elements are always
487+
/// non-negative — `validate_view` enforces this.
488+
fn shape(&self) -> &[i64];
480489

481490
/// **Internal/FFI-only.** Natural C-order extent of the band's
482491
/// underlying `data` buffer, indexed by *source* axis (not visible
@@ -489,17 +498,23 @@ pub trait BandRef {
489498
/// Use this only when you need to index directly into the raw `data`
490499
/// bytes (e.g. Arrow C Data Interface, numpy zero-copy views) and you
491500
/// also handle `view()` and the byte-stride layout from `nd_buffer()`.
501+
///
502+
/// Returns `&[u64]` because this is a direct slice into the underlying
503+
/// Arrow `UInt64Array` column; converting would require an allocation
504+
/// per call. Callers that want i64 should cast at the call site.
492505
fn raw_source_shape(&self) -> &[u64];
493506

494507
/// Per-visible-dimension view entries describing how the band's
495508
/// visible axes map onto its `source_shape`. `view().len() == ndim()`.
496509
/// See `ViewEntry` for per-entry semantics.
497510
fn view(&self) -> &[ViewEntry];
498511

499-
/// Size of a named dimension (None if doesn't exist)
512+
/// Size of a named dimension (None if doesn't exist).
513+
/// Cast i64 → u64 is lossless: `shape()` entries are non-negative
514+
/// (`validate_view` enforces it).
500515
fn dim_size(&self, name: &str) -> Option<u64> {
501516
let idx = self.dim_index(name)?;
502-
Some(self.shape()[idx])
517+
Some(self.shape()[idx] as u64)
503518
}
504519

505520
/// Index of a named dimension (None if doesn't exist)
@@ -678,13 +693,10 @@ pub trait BandRef {
678693
/// `view[k].steps` is the visible extent along view axis `k` after slicing /
679694
/// broadcasting. Callers should treat the returned shape as authoritative for
680695
/// the visible region; `source_shape` is only meaningful in conjunction with
681-
/// the per-entry `source_axis`.
682-
///
683-
/// `validate_view` guarantees `steps >= 0`, so the `as u64` cast is lossless
684-
/// when the input has already been validated. Callers that haven't validated
685-
/// yet should still call `validate_view` first.
686-
pub(crate) fn visible_shape_from_view(view: &[ViewEntry]) -> Vec<u64> {
687-
view.iter().map(|v| v.steps as u64).collect()
696+
/// the per-entry `source_axis`. `validate_view` guarantees `steps >= 0`,
697+
/// so the entries are all non-negative when input has been validated.
698+
pub(crate) fn visible_shape_from_view(view: &[ViewEntry]) -> Vec<i64> {
699+
view.iter().map(|v| v.steps).collect()
688700
}
689701

690702
/// True iff `view` is the canonical identity over a C-order source buffer:
@@ -698,16 +710,12 @@ pub(crate) fn visible_shape_from_view(view: &[ViewEntry]) -> Vec<u64> {
698710
///
699711
/// Callers should validate the view first; this function checks lengths
700712
/// defensively but trusts the inputs otherwise.
701-
pub(crate) fn is_identity_view(view: &[ViewEntry], source_shape: &[u64]) -> bool {
713+
pub(crate) fn is_identity_view(view: &[ViewEntry], source_shape: &[i64]) -> bool {
702714
if view.len() != source_shape.len() {
703715
return false;
704716
}
705717
view.iter().enumerate().all(|(k, v)| {
706-
v.source_axis == k as i64
707-
&& v.start == 0
708-
&& v.step == 1
709-
&& v.steps >= 0
710-
&& (v.steps as u64) == source_shape[k]
718+
v.source_axis == k as i64 && v.start == 0 && v.step == 1 && v.steps == source_shape[k]
711719
})
712720
}
713721

@@ -789,7 +797,7 @@ pub(crate) fn compose_view(
789797
///
790798
/// Runs implicitly inside `RasterBuilder::with_view` (writer) and
791799
/// `RasterRef::band` (reader); external callers don't need to invoke it.
792-
pub(crate) fn validate_view(view: &[ViewEntry], source_shape: &[u64]) -> Result<(), ArrowError> {
800+
pub(crate) fn validate_view(view: &[ViewEntry], source_shape: &[i64]) -> Result<(), ArrowError> {
793801
let ndim = source_shape.len();
794802
if view.len() != ndim {
795803
return Err(ArrowError::InvalidArgumentError(format!(
@@ -820,8 +828,14 @@ pub(crate) fn validate_view(view: &[ViewEntry], source_shape: &[u64]) -> Result<
820828
v.steps
821829
)));
822830
}
831+
if source_shape[sa] < 0 {
832+
return Err(ArrowError::InvalidArgumentError(format!(
833+
"source_shape[{sa}] = {} must be >= 0",
834+
source_shape[sa]
835+
)));
836+
}
823837
if v.steps > 0 {
824-
let s = source_shape[sa] as i64;
838+
let s = source_shape[sa];
825839
if v.start < 0 || v.start >= s {
826840
return Err(ArrowError::InvalidArgumentError(format!(
827841
"view[{k}].start = {} is out of range [0, {s}) for source axis {sa}",
@@ -1319,7 +1333,7 @@ mod tests {
13191333
struct StubBand {
13201334
dim_names: Vec<String>,
13211335
source_shape: Vec<u64>,
1322-
shape: Vec<u64>,
1336+
shape: Vec<i64>,
13231337
view: Vec<ViewEntry>,
13241338
}
13251339

@@ -1330,7 +1344,7 @@ mod tests {
13301344
fn dim_names(&self) -> Vec<&str> {
13311345
self.dim_names.iter().map(String::as_str).collect()
13321346
}
1333-
fn shape(&self) -> &[u64] {
1347+
fn shape(&self) -> &[i64] {
13341348
&self.shape
13351349
}
13361350
fn raw_source_shape(&self) -> &[u64] {

0 commit comments

Comments
 (0)