Skip to content

Commit 570d358

Browse files
committed
vortex-row: codec for nested canonical types
Extend the codec to handle Struct, FixedSizeList, and Extension canonical variants. Each nested row encodes as `outer_sentinel | child bytes...`; for null rows the child bytes are zero-filled after the recursive encoders run so two null rows compare equal regardless of which non-null values would have been written by the children. `row_width_for_dtype` recurses through Struct fields and FSL elements to return `Fixed(w)` when every leaf is fixed; otherwise `Variable`. Extension delegates to its storage dtype. List remains `Variable` and ListView still bails (the row encoder's output is itself a ListView, so nested ListView isn't a near-term use case). Variant and Union bail explicitly. Signed-off-by: Claude <noreply@anthropic.com>
1 parent d3f3da4 commit 570d358

1 file changed

Lines changed: 216 additions & 11 deletions

File tree

vortex-row/src/codec.rs

Lines changed: 216 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,15 @@ use vortex_array::ExecutionCtx;
3030
use vortex_array::accessor::ArrayAccessor;
3131
use vortex_array::arrays::BoolArray;
3232
use vortex_array::arrays::DecimalArray;
33+
use vortex_array::arrays::ExtensionArray;
34+
use vortex_array::arrays::FixedSizeListArray;
3335
use vortex_array::arrays::NullArray;
3436
use vortex_array::arrays::PrimitiveArray;
37+
use vortex_array::arrays::StructArray;
3538
use vortex_array::arrays::VarBinViewArray;
39+
use vortex_array::arrays::extension::ExtensionArrayExt;
40+
use vortex_array::arrays::fixed_size_list::FixedSizeListArrayExt;
41+
use vortex_array::arrays::struct_::StructArrayExt;
3642
use vortex_array::dtype::DType;
3743
use vortex_array::dtype::DecimalType;
3844
use vortex_array::dtype::NativePType;
@@ -112,9 +118,28 @@ pub fn row_width_for_dtype(dtype: &DType) -> VortexResult<RowWidth> {
112118
)))
113119
}
114120
DType::Utf8(_) | DType::Binary(_) => Ok(RowWidth::Variable),
115-
DType::Struct(..) | DType::FixedSizeList(..) | DType::List(..) | DType::Extension(..) => {
116-
vortex_bail!("row encoding for {} is not yet supported", dtype)
121+
DType::FixedSizeList(elem, n, _) => match row_width_for_dtype(elem)? {
122+
// FSL is fixed iff its element type is fixed. Add a sentinel byte for the FSL
123+
// itself, then `n` copies of the element width.
124+
RowWidth::Fixed(w) => {
125+
let body = w.saturating_mul(*n);
126+
Ok(RowWidth::Fixed(body.saturating_add(1)))
127+
}
128+
RowWidth::Variable => Ok(RowWidth::Variable),
129+
},
130+
DType::Struct(fields, _) => {
131+
// Struct is fixed iff all its fields are fixed; sum their widths plus a sentinel.
132+
let mut total: u32 = 1; // outer sentinel
133+
for field_dtype in fields.fields() {
134+
match row_width_for_dtype(&field_dtype)? {
135+
RowWidth::Fixed(w) => total = total.saturating_add(w),
136+
RowWidth::Variable => return Ok(RowWidth::Variable),
137+
}
138+
}
139+
Ok(RowWidth::Fixed(total))
117140
}
141+
DType::List(..) => Ok(RowWidth::Variable),
142+
DType::Extension(ext) => row_width_for_dtype(ext.storage_dtype()),
118143
DType::Variant(_) => {
119144
vortex_bail!("row encoding does not support Variant arrays (no well-defined ordering)")
120145
}
@@ -133,7 +158,7 @@ pub fn row_width_for_dtype(dtype: &DType) -> VortexResult<RowWidth> {
133158
/// variants land in later commits.
134159
pub fn field_size(
135160
canonical: &Canonical,
136-
_field: SortField,
161+
field: SortField,
137162
sizes: &mut [u32],
138163
ctx: &mut ExecutionCtx,
139164
) -> VortexResult<()> {
@@ -143,10 +168,10 @@ pub fn field_size(
143168
Canonical::Primitive(arr) => add_size_primitive(arr, sizes),
144169
Canonical::Decimal(arr) => add_size_decimal(arr, sizes),
145170
Canonical::VarBinView(arr) => add_size_varbinview(arr, sizes, ctx)?,
146-
Canonical::Struct(_)
147-
| Canonical::FixedSizeList(_)
148-
| Canonical::Extension(_)
149-
| Canonical::List(_) => vortex_bail!(
171+
Canonical::Struct(arr) => add_size_struct(arr, field, sizes, ctx)?,
172+
Canonical::FixedSizeList(arr) => add_size_fsl(arr, field, sizes, ctx)?,
173+
Canonical::Extension(arr) => add_size_extension(arr, field, sizes, ctx)?,
174+
Canonical::List(_) => vortex_bail!(
150175
"row encoding does not yet support canonical type {:?}",
151176
canonical.dtype()
152177
),
@@ -177,10 +202,10 @@ pub fn field_encode(
177202
Canonical::Primitive(arr) => encode_primitive(arr, field, offsets, cursors, out, ctx)?,
178203
Canonical::Decimal(arr) => encode_decimal(arr, field, offsets, cursors, out, ctx)?,
179204
Canonical::VarBinView(arr) => encode_varbinview(arr, field, offsets, cursors, out, ctx)?,
180-
Canonical::Struct(_)
181-
| Canonical::FixedSizeList(_)
182-
| Canonical::Extension(_)
183-
| Canonical::List(_) => vortex_bail!(
205+
Canonical::Struct(arr) => encode_struct(arr, field, offsets, cursors, out, ctx)?,
206+
Canonical::FixedSizeList(arr) => encode_fsl(arr, field, offsets, cursors, out, ctx)?,
207+
Canonical::Extension(arr) => encode_extension(arr, field, offsets, cursors, out, ctx)?,
208+
Canonical::List(_) => vortex_bail!(
184209
"row encoding does not yet support canonical type {:?}",
185210
canonical.dtype()
186211
),
@@ -234,6 +259,60 @@ fn add_size_varbinview(
234259
Ok(())
235260
}
236261

262+
fn add_size_struct(
263+
arr: &StructArray,
264+
field: SortField,
265+
sizes: &mut [u32],
266+
ctx: &mut ExecutionCtx,
267+
) -> VortexResult<()> {
268+
// null sentinel: 1 byte per row.
269+
for s in sizes.iter_mut() {
270+
*s += 1;
271+
}
272+
// Each field adds its own per-row size.
273+
for child in arr.iter_unmasked_fields() {
274+
let canonical = child.clone().execute::<Canonical>(ctx)?;
275+
field_size(&canonical, field, sizes, ctx)?;
276+
}
277+
Ok(())
278+
}
279+
280+
fn add_size_fsl(
281+
arr: &FixedSizeListArray,
282+
field: SortField,
283+
sizes: &mut [u32],
284+
ctx: &mut ExecutionCtx,
285+
) -> VortexResult<()> {
286+
let n = arr.len();
287+
debug_assert_eq!(n, sizes.len());
288+
let list_size = arr.list_size() as usize;
289+
let elements = arr.elements().clone().execute::<Canonical>(ctx)?;
290+
debug_assert_eq!(elements.len(), n * list_size);
291+
// Sizing: 1 sentinel + sum of element sizes (`list_size` per row).
292+
// We compute element-wise sizes into a contiguous scratch buffer then reduce by row.
293+
let mut elem_sizes = vec![0u32; n * list_size];
294+
field_size(&elements, field, &mut elem_sizes, ctx)?;
295+
for i in 0..n {
296+
let mut sum: u32 = 1; // sentinel
297+
let base = i * list_size;
298+
for j in 0..list_size {
299+
sum = sum.saturating_add(elem_sizes[base + j]);
300+
}
301+
sizes[i] += sum;
302+
}
303+
Ok(())
304+
}
305+
306+
fn add_size_extension(
307+
arr: &ExtensionArray,
308+
field: SortField,
309+
sizes: &mut [u32],
310+
ctx: &mut ExecutionCtx,
311+
) -> VortexResult<()> {
312+
let storage = arr.storage_array().clone().execute::<Canonical>(ctx)?;
313+
field_size(&storage, field, sizes, ctx)
314+
}
315+
237316
fn encode_null(
238317
arr: &NullArray,
239318
field: SortField,
@@ -413,6 +492,132 @@ fn encode_varbinview(
413492
Ok(())
414493
}
415494

495+
fn encode_struct(
496+
arr: &StructArray,
497+
field: SortField,
498+
row_offsets: &[u32],
499+
col_offset: &mut [u32],
500+
out: &mut [u8],
501+
ctx: &mut ExecutionCtx,
502+
) -> VortexResult<()> {
503+
let n = arr.len();
504+
let mask = arr.as_ref().validity()?.execute_mask(n, ctx)?;
505+
let non_null = field.non_null_sentinel();
506+
let null = field.null_sentinel();
507+
508+
// First, write the sentinel for each row. We track the post-sentinel cursor offsets
509+
// for the body in `body_cursors` (which start exactly at +1 of the input cursor).
510+
// For null rows we additionally need to zero-fill the (uniform-width) field bytes,
511+
// but because struct widths are variable in general, we record null indexes first
512+
// and zero-fill after we know each row's contribution.
513+
//
514+
// To keep the implementation simple we:
515+
// 1) advance the cursor past the sentinel,
516+
// 2) recursively encode each field's bytes (the field encoders ignore nullness of
517+
// the struct, but use their own per-field nullness),
518+
// 3) for null struct rows, overwrite the body bytes with zeros so the encoded form
519+
// depends only on the sentinel.
520+
let body_start: Vec<u32> = (0..n).map(|i| col_offset[i] + 1).collect();
521+
for i in 0..n {
522+
let pos = (row_offsets[i] + col_offset[i]) as usize;
523+
out[pos] = if mask.value(i) { non_null } else { null };
524+
col_offset[i] += 1;
525+
}
526+
527+
for child in arr.iter_unmasked_fields() {
528+
let canonical = child.clone().execute::<Canonical>(ctx)?;
529+
field_encode(&canonical, field, row_offsets, col_offset, out, ctx)?;
530+
}
531+
532+
// Zero-fill body bytes of null rows (the field encoders may have written values).
533+
for i in 0..n {
534+
if !mask.value(i) {
535+
let start = (row_offsets[i] + body_start[i]) as usize;
536+
let end = (row_offsets[i] + col_offset[i]) as usize;
537+
for b in &mut out[start..end] {
538+
*b = 0;
539+
}
540+
}
541+
}
542+
543+
Ok(())
544+
}
545+
546+
fn encode_fsl(
547+
arr: &FixedSizeListArray,
548+
field: SortField,
549+
row_offsets: &[u32],
550+
col_offset: &mut [u32],
551+
out: &mut [u8],
552+
ctx: &mut ExecutionCtx,
553+
) -> VortexResult<()> {
554+
let n = arr.len();
555+
let list_size = arr.list_size() as usize;
556+
let mask = arr.as_ref().validity()?.execute_mask(n, ctx)?;
557+
let non_null = field.non_null_sentinel();
558+
let null = field.null_sentinel();
559+
let elements = arr.elements().clone().execute::<Canonical>(ctx)?;
560+
debug_assert_eq!(elements.len(), n * list_size);
561+
562+
// Write sentinels and remember body start for null zero-fill.
563+
let body_start: Vec<u32> = (0..n).map(|i| col_offset[i] + 1).collect();
564+
for i in 0..n {
565+
let pos = (row_offsets[i] + col_offset[i]) as usize;
566+
out[pos] = if mask.value(i) { non_null } else { null };
567+
col_offset[i] += 1;
568+
}
569+
570+
// Encode all `n * list_size` elements into the body. Build a fresh
571+
// (offsets, cursors) pair where each element gets one slot. Then sum bytes back
572+
// into the parent col_offset.
573+
let mut elem_sizes = vec![0u32; n * list_size];
574+
field_size(&elements, field, &mut elem_sizes, ctx)?;
575+
// Element offsets are sequential starting at each parent's current cursor position.
576+
let mut elem_offsets = vec![0u32; n * list_size];
577+
for i in 0..n {
578+
let mut acc = row_offsets[i] + col_offset[i];
579+
for j in 0..list_size {
580+
elem_offsets[i * list_size + j] = acc;
581+
acc = acc.saturating_add(elem_sizes[i * list_size + j]);
582+
}
583+
}
584+
let mut elem_cursors = vec![0u32; n * list_size];
585+
field_encode(&elements, field, &elem_offsets, &mut elem_cursors, out, ctx)?;
586+
// Advance the parent cursors by the total per-row element bytes.
587+
for i in 0..n {
588+
let mut sum: u32 = 0;
589+
for j in 0..list_size {
590+
sum = sum.saturating_add(elem_sizes[i * list_size + j]);
591+
}
592+
col_offset[i] = col_offset[i].saturating_add(sum);
593+
}
594+
595+
// Zero-fill null bodies.
596+
for i in 0..n {
597+
if !mask.value(i) {
598+
let start = (row_offsets[i] + body_start[i]) as usize;
599+
let end = (row_offsets[i] + col_offset[i]) as usize;
600+
for b in &mut out[start..end] {
601+
*b = 0;
602+
}
603+
}
604+
}
605+
606+
Ok(())
607+
}
608+
609+
fn encode_extension(
610+
arr: &ExtensionArray,
611+
field: SortField,
612+
row_offsets: &[u32],
613+
col_offset: &mut [u32],
614+
out: &mut [u8],
615+
ctx: &mut ExecutionCtx,
616+
) -> VortexResult<()> {
617+
let storage = arr.storage_array().clone().execute::<Canonical>(ctx)?;
618+
field_encode(&storage, field, row_offsets, col_offset, out, ctx)
619+
}
620+
416621
/// Encode a variable-length byte slice into `out` in 32-byte blocks with
417622
/// continuation markers. Returns the number of bytes written.
418623
fn encode_varlen_value(bytes: &[u8], out: &mut [u8], descending: bool) -> u32 {

0 commit comments

Comments
 (0)