james-willis commented on code in PR #749:
URL: https://github.com/apache/sedona-db/pull/749#discussion_r3120229476


##########
rust/sedona-schema/src/raster.rs:
##########
@@ -55,32 +54,37 @@ impl RasterSchema {
         )))
     }
 
-    /// Individual band schema
+    /// Individual band schema — flattened N-D band with dimension metadata
     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),
+            Field::new(column::NAME, DataType::Utf8, true),
+            Field::new(column::DIM_NAMES, Self::dim_names_type(), false),
+            Field::new(column::SHAPE, Self::shape_type(), false),
+            Field::new(column::DATATYPE, DataType::UInt32, false),
+            Field::new(column::NODATA, DataType::Binary, true),
+            Field::new(column::STRIDES, Self::strides_type(), false),
+            Field::new(column::OFFSET, DataType::UInt64, false),
+            Field::new(column::OUTDB_URI, DataType::Utf8, true),
+            Field::new(column::DATA, DataType::BinaryView, false),

Review Comment:
   Thinking about this more, I need to correct something in my earlier stacking 
comment. I said stacking breaks at *shuffles*, but that was too optimistic — in 
DataFusion, stacking breaks at **every UDF boundary**,
     shuffle or not. Every scalar UDF receives `ColumnarValue` (Arrow) and 
returns `ColumnarValue`, so `Arc<dyn BandRef>` wrappers can't live across 
`RS_A(RS_B(...))` without extra machinery. The stacking model is
     still the right *internal* data model — it's what lets a single kernel 
compose lazy views — but "stacking survives normal UDF chaining for free" was 
wrong.
   
     That correction actually makes the strides/offset story cleaner, because 
now it's the same question in both places: *how do we compress a stack into 
something Arrow can carry to the next UDF?*
   
     **View-producing stacks compress losslessly.** Any composition of 
`StridedBandRef`-shaped operations — `RS_Slice`, `RS_SliceRange`, 
`RS_DimToBand`, transposes, constant broadcasts (stride=0) — collapses exactly 
into four integers (strides, offset, shape) plus an unmodified `outdb_uri`. So 
`RS_Slice(RS_DimToBand(zarr_raster, 'wavelength'), 'time', 0)` serializes to 
the next UDF with `outdb_uri` preserved, strides/offset/shape updated, empty 
`data`. Full laziness survives the Arrow round-trip; the downstream 
`ZarrBandRef` reconstitutes on read. This is exactly what strides/offset buys 
us, and it's why I want to keep those fields as typed ints rather than a subset 
string.
   
     **Compute-producing stacks compress lossily.** The moment a 
`CoarsenBandRef` (or Normalize, MapAlgebra, Resample) is in the stack, we're 
producing new pixel values — there's no four-integer view that represents 
"coarsened." At the next UDF boundary it has to materialize. Under Phase 1 that 
means `RS_Width(RS_Coarsen(r, 2))` pays full materialization cost even though 
`Width` only needs `shape`. A `subset: Utf8` doesn't help here either — a 
subset DSL can encode ranges/strides/index lists, but not "coarsened."
   
     **The clean future-work answer is operator fusion at plan time.** An 
`ExprRewriter` rule detects chains like `RS_Width(RS_Coarsen(RS_Rechunk(r)))` 
and rewrites them to a single `RasterPipeline` meta-UDF. Inside
     one kernel, the stacking model works exactly as I first described — 
`StridedBandRef` → `CoarsenBandRef` → `.shape()` — and Arrow serialization only 
happens at the outermost boundary. Fusion is what makes a
     multi-UDF chain behave like the single-UDF-internal case where stacking 
was always going to work. No schema change, no trait change; can land whenever 
the compute-chain pattern shows up as a real bottleneck.
   
     So the revised picture:
   
     - Phase 1 as drafted: view-producing stacks stay lazy across UDF 
boundaries via strides/offset + preserved `outdb_uri`. Compute-producing stacks 
materialize at the boundary.
     - Future: operator fusion lets compute-producing stacks stay lazy too, 
without changing the schema we land now.
   
     On `subset: Utf8` specifically — tempting, but the trade-offs point the 
other way:
   
     - Parser/validator surface: any DSL needs parsing, error messages, docs, 
version stability. Strides/offset are four integers.
     - No laziness advantage over strides/offset for view cases, no help at all 
for compute cases — the real axis is fusion, not subset expressiveness.
     - Interop: strides/offset is the pybuffer / numpy / Arrow FFI vocabulary — 
a numpy consumer memory-maps our buffer directly. A DSL needs translation.
     - The one thing strides/offset can't express is non-contiguous index lists 
(e.g. `y ∈ {2, 3, 5}`); I'd handle that with materialize-then-stride when it 
comes up.
   
     If we hit a case strides/offset can't express *and* fusion can't handle, 
we can revisit. But for Phase 1 I'd rather keep the schema narrow and treat 
fusion as deferred work



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to