Skip to content

Commit 9238470

Browse files
committed
Fix UnionArray canonicalization edge cases
Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent a56fb4f commit 9238470

3 files changed

Lines changed: 168 additions & 18 deletions

File tree

vortex-array/src/arrays/constant/vtable/canonical.rs

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use vortex_buffer::Buffer;
88
use vortex_buffer::buffer;
99
use vortex_error::VortexExpect;
1010
use vortex_error::VortexResult;
11+
use vortex_error::vortex_err;
1112

1213
use crate::Canonical;
1314
use crate::ExecutionCtx;
@@ -149,16 +150,22 @@ pub(crate) fn constant_canonicalize(
149150
.collect(),
150151
None => {
151152
assert!(matches!(validity, Validity::AllInvalid));
152-
// The struct is entirely null, so fields just need placeholder values with the
153-
// correct dtype. We use `default_value` which returns a zero for non-nullable
154-
// dtypes and null for nullable dtypes, preserving each field's nullability.
155153
struct_dtype
156154
.fields()
157155
.map(|dt| {
158-
let scalar = Scalar::default_value(&dt);
159-
ConstantArray::new(scalar, array.len()).into_array()
156+
if array.is_empty() {
157+
return Ok(Canonical::empty(&dt).into_array());
158+
}
159+
160+
let scalar = Scalar::try_default_value(&dt).ok_or_else(|| {
161+
vortex_err!(
162+
"cannot canonicalize null constant struct: field dtype {dt} \
163+
has no default value"
164+
)
165+
})?;
166+
Ok(ConstantArray::new(scalar, array.len()).into_array())
160167
})
161-
.collect()
168+
.collect::<VortexResult<Vec<_>>>()?
162169
}
163170
};
164171
// SAFETY: Fields are constructed from the same struct scalar, all have same
@@ -202,25 +209,32 @@ fn constant_canonical_union(
202209
nullability: Nullability,
203210
len: usize,
204211
) -> VortexResult<UnionArray> {
212+
if len == 0 {
213+
return Ok(UnionArray::empty(variants.clone(), nullability));
214+
}
215+
205216
let union = scalar.as_union();
206217
let selected = union.child_index().zip(union.value());
207218

208219
let children = variants
209220
.variants()
210221
.enumerate()
211222
.map(|(index, dtype)| {
212-
let value = match selected {
213-
Some((selected_child, selected_value)) if index == selected_child => {
223+
let value = match &selected {
224+
Some((selected_child, selected_value)) if index == *selected_child => {
214225
selected_value.clone()
215226
}
216-
_ if matches!(&dtype, DType::Union(_, Nullability::NonNullable)) => {
217-
Scalar::zero_value(&dtype)
218-
}
219-
_ => Scalar::default_value(&dtype),
227+
_ => Scalar::try_default_value(&dtype).ok_or_else(|| {
228+
vortex_err!(
229+
"cannot canonicalize constant union: unselected variant {} ({dtype}) has \
230+
no default value",
231+
variants.names()[index]
232+
)
233+
})?,
220234
};
221-
ConstantArray::new(value, len).into_array()
235+
Ok(ConstantArray::new(value, len).into_array())
222236
})
223-
.collect::<Vec<_>>();
237+
.collect::<VortexResult<Vec<_>>>()?;
224238

225239
let type_id = match union.type_id() {
226240
Some(type_id) => Scalar::primitive(type_id, nullability),

vortex-array/src/arrays/union/tests.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
use vortex_buffer::ByteBufferMut;
55
use vortex_buffer::buffer;
66
use vortex_error::VortexResult;
7+
use vortex_error::vortex_err;
78
use vortex_mask::Mask;
89
use vortex_session::registry::ReadContext;
910

1011
use crate::ArrayContext;
12+
use crate::Canonical;
13+
use crate::CanonicalValidity;
1114
use crate::IntoArray;
1215
use crate::VortexSessionExecute;
1316
use crate::array_session;
@@ -339,6 +342,121 @@ fn constant_union_builds_nested_union_placeholders() -> VortexResult<()> {
339342
Ok(())
340343
}
341344

345+
#[test]
346+
fn constant_union_builds_deeply_nested_union_placeholders() -> VortexResult<()> {
347+
let nested_dtype = DType::Union(
348+
UnionVariants::try_new(
349+
["value"].into(),
350+
vec![DType::Primitive(PType::I64, Nullability::NonNullable)],
351+
vec![3],
352+
)?,
353+
Nullability::NonNullable,
354+
);
355+
let wrapper_dtype =
356+
DType::struct_([("nested", nested_dtype.clone())], Nullability::NonNullable);
357+
let outer_variants = UnionVariants::try_new(
358+
["number", "wrapper"].into(),
359+
vec![
360+
DType::Primitive(PType::I32, Nullability::NonNullable),
361+
wrapper_dtype,
362+
],
363+
vec![5, 9],
364+
)?;
365+
let selected_number =
366+
Scalar::union(outer_variants, 5, 42_i32.into(), Nullability::NonNullable)?;
367+
let mut ctx = array_session().create_execution_ctx();
368+
let array = ConstantArray::new(selected_number, 2)
369+
.into_array()
370+
.execute::<UnionArray>(&mut ctx)?;
371+
let wrapper = array
372+
.child_by_name("wrapper")?
373+
.execute_scalar(0, &mut ctx)?;
374+
375+
assert_eq!(
376+
wrapper.as_struct().field("nested"),
377+
Some(Scalar::default_value(&nested_dtype))
378+
);
379+
380+
Ok(())
381+
}
382+
383+
#[test]
384+
fn constant_union_rejects_uninhabited_placeholders_without_panicking() -> VortexResult<()> {
385+
let uninhabited = DType::Union(
386+
UnionVariants::new(Default::default(), vec![])?,
387+
Nullability::NonNullable,
388+
);
389+
let outer_variants = UnionVariants::try_new(
390+
["number", "uninhabited"].into(),
391+
vec![
392+
DType::Primitive(PType::I32, Nullability::NonNullable),
393+
uninhabited,
394+
],
395+
vec![5, 9],
396+
)?;
397+
let selected_number =
398+
Scalar::union(outer_variants, 5, 42_i32.into(), Nullability::NonNullable)?;
399+
let mut ctx = array_session().create_execution_ctx();
400+
401+
assert!(
402+
ConstantArray::new(selected_number.clone(), 1)
403+
.into_array()
404+
.execute::<UnionArray>(&mut ctx)
405+
.is_err()
406+
);
407+
assert_eq!(
408+
ConstantArray::new(selected_number, 0)
409+
.into_array()
410+
.execute::<UnionArray>(&mut ctx)?
411+
.len(),
412+
0
413+
);
414+
415+
Ok(())
416+
}
417+
418+
#[test]
419+
fn empty_union_supports_variant_children() -> VortexResult<()> {
420+
let variants = UnionVariants::try_new(
421+
["dynamic"].into(),
422+
vec![DType::Variant(Nullability::NonNullable)],
423+
vec![5],
424+
)?;
425+
let array = Canonical::empty(&DType::Union(variants, Nullability::NonNullable)).into_union();
426+
427+
assert_eq!(array.len(), 0);
428+
assert_eq!(
429+
array.child(0).map(|child| child.dtype()),
430+
Some(&DType::Variant(Nullability::NonNullable))
431+
);
432+
433+
Ok(())
434+
}
435+
436+
#[test]
437+
fn canonical_validity_canonicalizes_union_type_ids() -> VortexResult<()> {
438+
let array = UnionArray::try_new(
439+
ConstantArray::new(Scalar::primitive(5_u8, Nullability::Nullable), 2).into_array(),
440+
variants()?,
441+
vec![
442+
PrimitiveArray::from_iter([10_i32, 20]).into_array(),
443+
BoolArray::from_iter([false, true]).into_array(),
444+
],
445+
)?;
446+
let mut ctx = array_session().create_execution_ctx();
447+
let Canonical::Union(array) = array.into_array().execute::<CanonicalValidity>(&mut ctx)?.0
448+
else {
449+
return Err(vortex_err!(
450+
"UnionArray must remain canonical Union storage"
451+
));
452+
};
453+
454+
assert!(array.type_ids().is::<crate::arrays::Primitive>());
455+
assert_eq!(array.dtype().nullability(), Nullability::Nullable);
456+
457+
Ok(())
458+
}
459+
342460
#[test]
343461
fn chunked_union_packs_components() -> VortexResult<()> {
344462
let first = union_array()?.into_array().slice(0..1)?;

vortex-array/src/canonical.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use crate::array::ArrayView;
2121
use crate::array::child_to_validity;
2222
use crate::arrays::Bool;
2323
use crate::arrays::BoolArray;
24+
use crate::arrays::ChunkedArray;
2425
use crate::arrays::Decimal;
2526
use crate::arrays::DecimalArray;
2627
use crate::arrays::Extension;
@@ -237,9 +238,15 @@ impl Canonical {
237238
DType::Union(variants, nullability) => {
238239
Canonical::Union(UnionArray::empty(variants.clone(), *nullability))
239240
}
240-
DType::Variant(_) => {
241-
vortex_panic!(InvalidArgument: "Canonical empty is not supported for Variant")
242-
}
241+
DType::Variant(_) => Canonical::Variant(
242+
VariantArray::try_new(
243+
ChunkedArray::try_new(vec![], dtype.clone())
244+
.vortex_expect("empty Variant core storage must be valid")
245+
.into_array(),
246+
None,
247+
)
248+
.vortex_expect("empty VariantArray must be valid"),
249+
),
243250
DType::Extension(ext_dtype) => Canonical::Extension(ExtensionArray::new(
244251
ext_dtype.clone(),
245252
Canonical::empty(ext_dtype.storage_dtype()).into_array(),
@@ -678,7 +685,18 @@ impl Executable for CanonicalValidity {
678685
StructArray::new_unchecked(fields, struct_fields, len, validity.execute(ctx)?)
679686
})))
680687
}
681-
union @ Canonical::Union(_) => Ok(CanonicalValidity(union)),
688+
Canonical::Union(union) => {
689+
let UnionDataParts {
690+
variants,
691+
type_ids,
692+
children,
693+
} = union.into_data_parts();
694+
let type_ids = type_ids.execute::<CanonicalValidity>(ctx)?.0.into_array();
695+
696+
Ok(CanonicalValidity(Canonical::Union(unsafe {
697+
UnionArray::new_unchecked(type_ids, variants, children)
698+
})))
699+
}
682700
Canonical::Extension(ext) => Ok(CanonicalValidity(Canonical::Extension(
683701
ExtensionArray::new(
684702
ext.ext_dtype().clone(),

0 commit comments

Comments
 (0)