Skip to content

Commit 10e2190

Browse files
committed
Implement nullable UnionArray semantics
Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent adb5991 commit 10e2190

14 files changed

Lines changed: 311 additions & 119 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ use crate::arrays::chunked::ChunkedArrayExt;
2525
use crate::arrays::fixed_size_list::FixedSizeListArrayExt;
2626
use crate::arrays::listview::ListViewArrayExt;
2727
use crate::arrays::listview::ListViewRebuildMode;
28-
use crate::arrays::union::TYPE_IDS_DTYPE;
2928
use crate::arrays::union::UnionArrayExt;
29+
use crate::arrays::union::type_ids_dtype;
3030
use crate::arrays::variant::VariantArrayExt;
3131
use crate::builders::builder_with_capacity_in;
3232
use crate::builtins::ArrayBuiltins;
@@ -105,7 +105,7 @@ fn pack_union_chunks(chunks: Vec<ArrayRef>, ctx: &mut ExecutionCtx) -> VortexRes
105105
.iter()
106106
.map(|chunk| chunk.type_ids().clone())
107107
.collect(),
108-
TYPE_IDS_DTYPE,
108+
type_ids_dtype(union_chunks[0].dtype().nullability()),
109109
)?
110110
.into_array();
111111
let children = variants

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

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ use vortex_buffer::Buffer;
88
use vortex_buffer::buffer;
99
use vortex_error::VortexExpect;
1010
use vortex_error::VortexResult;
11-
use vortex_error::vortex_bail;
1211

1312
use crate::Canonical;
1413
use crate::ExecutionCtx;
@@ -32,6 +31,7 @@ use crate::builders::builder_with_capacity;
3231
use crate::dtype::DType;
3332
use crate::dtype::DecimalType;
3433
use crate::dtype::Nullability;
34+
use crate::dtype::PType;
3535
use crate::dtype::UnionVariants;
3636
use crate::match_each_decimal_value;
3737
use crate::match_each_decimal_value_type;
@@ -167,9 +167,12 @@ pub(crate) fn constant_canonicalize(
167167
StructArray::new_unchecked(fields, struct_dtype.clone(), array.len(), validity)
168168
})
169169
}
170-
DType::Union(variants) => {
171-
Canonical::Union(constant_canonical_union(scalar, variants, array.len())?)
172-
}
170+
DType::Union(variants, nullability) => Canonical::Union(constant_canonical_union(
171+
scalar,
172+
variants,
173+
*nullability,
174+
array.len(),
175+
)?),
173176
DType::Variant(_) => Canonical::Variant(VariantArray::try_new(
174177
array.array().clone().into_array(),
175178
None,
@@ -196,41 +199,31 @@ pub(crate) fn constant_canonicalize(
196199
fn constant_canonical_union(
197200
scalar: &Scalar,
198201
variants: &UnionVariants,
202+
nullability: Nullability,
199203
len: usize,
200204
) -> VortexResult<UnionArray> {
201-
if scalar.is_null() {
202-
vortex_bail!(
203-
"Canonicalizing a null union scalar is not supported until null semantics are defined"
204-
)
205-
}
206-
if variants.variants().any(|variant| variant.is_nullable()) {
207-
vortex_bail!("Canonical UnionArray children must be non-nullable")
208-
}
209-
210205
let union = scalar.as_union();
211-
let type_id = union
212-
.type_id()
213-
.vortex_expect("non-null union scalar must have a type ID");
214-
let selected_child = union
215-
.child_index()
216-
.vortex_expect("validated union scalar must select a child");
217-
let selected_value = union
218-
.value()
219-
.vortex_expect("non-null union scalar must have a value");
206+
let selected = union.child_index().zip(union.value());
220207

221208
let children = variants
222209
.variants()
223210
.enumerate()
224211
.map(|(index, dtype)| {
225-
let value = if index == selected_child {
226-
selected_value.clone()
227-
} else {
228-
Scalar::zero_value(&dtype)
212+
let value = match selected {
213+
Some((selected_child, selected_value)) if index == selected_child => {
214+
selected_value.clone()
215+
}
216+
_ => Scalar::default_value(&dtype),
229217
};
230218
ConstantArray::new(value, len).into_array()
231219
})
232220
.collect::<Vec<_>>();
233221

222+
let type_id = match union.type_id() {
223+
Some(type_id) => Scalar::primitive(type_id, nullability),
224+
None => Scalar::null(DType::Primitive(PType::U8, Nullability::Nullable)),
225+
};
226+
234227
UnionArray::try_new(
235228
ConstantArray::new(type_id, len).into_array(),
236229
variants.clone(),

vortex-array/src/arrays/masked/execute.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
use std::sync::Arc;
77

88
use vortex_error::VortexResult;
9-
use vortex_error::vortex_bail;
109

1110
use crate::Canonical;
1211
use crate::IntoArray;
@@ -18,13 +17,15 @@ use crate::arrays::ListViewArray;
1817
use crate::arrays::MaskedArray;
1918
use crate::arrays::PrimitiveArray;
2019
use crate::arrays::StructArray;
20+
use crate::arrays::UnionArray;
2121
use crate::arrays::VarBinViewArray;
2222
use crate::arrays::VariantArray;
2323
use crate::arrays::bool::BoolArrayExt;
2424
use crate::arrays::extension::ExtensionArrayExt;
2525
use crate::arrays::fixed_size_list::FixedSizeListArrayExt;
2626
use crate::arrays::listview::ListViewArrayExt;
2727
use crate::arrays::struct_::StructArrayExt;
28+
use crate::arrays::union::UnionArrayExt;
2829
use crate::arrays::variant::VariantArrayExt;
2930
use crate::builtins::ArrayBuiltins;
3031
use crate::executor::ExecutionCtx;
@@ -51,9 +52,7 @@ pub fn mask_validity_canonical(
5152
Canonical::FixedSizeList(mask_validity_fixed_size_list(a, validity)?)
5253
}
5354
Canonical::Struct(a) => Canonical::Struct(mask_validity_struct(a, validity)?),
54-
Canonical::Union(_) => {
55-
vortex_bail!("Masking UnionArray is not supported until null semantics are defined")
56-
}
55+
Canonical::Union(a) => Canonical::Union(mask_validity_union(a, validity)?),
5756
Canonical::Extension(a) => Canonical::Extension(mask_validity_extension(a, validity, ctx)?),
5857
Canonical::Variant(a) => Canonical::Variant(mask_validity_variant(a, validity, ctx)?),
5958
})
@@ -146,6 +145,19 @@ fn mask_validity_struct(array: StructArray, validity: Validity) -> VortexResult<
146145
Ok(unsafe { StructArray::new_unchecked(fields, struct_fields.clone(), len, new_validity) })
147146
}
148147

148+
fn mask_validity_union(array: UnionArray, validity: Validity) -> VortexResult<UnionArray> {
149+
let type_ids = array
150+
.type_ids()
151+
.clone()
152+
.mask(validity.to_array(array.len()))?;
153+
let variants = array.variants().clone();
154+
let children = array.children();
155+
156+
// SAFETY: Masking type IDs changes only outer validity. Values at newly-null positions are no
157+
// longer selected, and every row-aligned component retains its length and dtype.
158+
Ok(unsafe { UnionArray::new_unchecked(type_ids, variants, children) })
159+
}
160+
149161
fn mask_validity_extension(
150162
array: ExtensionArray,
151163
validity: Validity,

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

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ use crate::array::EmptyArrayData;
1919
use crate::array::TypedArrayRef;
2020
use crate::arrays::PrimitiveArray;
2121
use crate::arrays::Union;
22-
use crate::arrays::union::TYPE_IDS_DTYPE;
2322
use crate::dtype::DType;
23+
use crate::dtype::Nullability;
2424
use crate::dtype::UnionVariants;
2525
use crate::legacy_session;
2626

@@ -35,11 +35,18 @@ pub(super) fn make_union_parts(
3535
children: &[ArrayRef],
3636
) -> ArrayParts<Union> {
3737
let len = type_ids.len();
38+
let nullability = type_ids.dtype().nullability();
3839
let slots: ArraySlots = once(Some(type_ids))
3940
.chain(children.iter().cloned().map(Some))
4041
.collect();
4142

42-
ArrayParts::new(Union, DType::Union(variants), len, EmptyArrayData).with_slots(slots)
43+
ArrayParts::new(
44+
Union,
45+
DType::Union(variants, nullability),
46+
len,
47+
EmptyArrayData,
48+
)
49+
.with_slots(slots)
4350
}
4451

4552
/// Concrete parts of a [`UnionArray`](super::UnionArray).
@@ -57,12 +64,12 @@ pub trait UnionArrayExt: TypedArrayRef<Union> {
5764
/// The union's variant schema.
5865
fn variants(&self) -> &UnionVariants {
5966
match self.as_ref().dtype() {
60-
DType::Union(variants) => variants,
67+
DType::Union(variants, _) => variants,
6168
_ => unreachable!("UnionArrayExt requires a union dtype"),
6269
}
6370
}
6471

65-
/// The row-aligned non-nullable `i8` type IDs.
72+
/// The row-aligned `u8` type IDs whose nulls represent outer union nulls.
6673
fn type_ids(&self) -> &ArrayRef {
6774
self.as_ref().slots()[TYPE_IDS_SLOT]
6875
.as_ref()
@@ -87,7 +94,7 @@ pub trait UnionArrayExt: TypedArrayRef<Union> {
8794
}
8895

8996
/// Return a sparse child selected by a data-level type ID.
90-
fn child_by_type_id(&self, type_id: i8) -> Option<&ArrayRef> {
97+
fn child_by_type_id(&self, type_id: u8) -> Option<&ArrayRef> {
9198
self.child(self.variants().tag_to_child_index(type_id)?)
9299
}
93100

@@ -118,13 +125,22 @@ fn validate_type_id_values(array: &Array<Union>) -> VortexResult<()> {
118125
return Ok(());
119126
}
120127

128+
let mut ctx = legacy_session().create_execution_ctx();
121129
let type_ids = array
122130
.type_ids()
123131
.clone()
124-
.execute::<PrimitiveArray>(&mut legacy_session().create_execution_ctx())?;
125-
for type_id in type_ids.as_slice::<i8>() {
132+
.execute::<PrimitiveArray>(&mut ctx)?;
133+
let values = type_ids.as_slice::<u8>();
134+
let validity = type_ids.validity()?.execute_mask(array.len(), &mut ctx)?;
135+
let valid_indices: Box<dyn Iterator<Item = usize> + '_> = match &validity {
136+
vortex_mask::Mask::AllTrue(_) => Box::new(0..values.len()),
137+
vortex_mask::Mask::AllFalse(_) => Box::new(std::iter::empty()),
138+
vortex_mask::Mask::Values(mask) => Box::new(mask.indices().iter().copied()),
139+
};
140+
for index in valid_indices {
141+
let type_id = values[index];
126142
vortex_ensure!(
127-
array.variants().tag_to_child_index(*type_id).is_some(),
143+
array.variants().tag_to_child_index(type_id).is_some(),
128144
"UnionArray type ID {type_id} is not present in {:?}",
129145
array.variants().type_ids()
130146
);
@@ -150,8 +166,6 @@ impl Array<Union> {
150166

151167
/// Try to construct a canonical sparse union array.
152168
///
153-
/// Until Union nullability semantics are settled, every child must be non-nullable.
154-
///
155169
/// # Errors
156170
///
157171
/// Returns an error if the type IDs and sparse children do not match the variant schema or do
@@ -162,8 +176,11 @@ impl Array<Union> {
162176
children: impl Into<Arc<[ArrayRef]>>,
163177
) -> VortexResult<Self> {
164178
vortex_ensure!(
165-
type_ids.dtype() == &TYPE_IDS_DTYPE,
166-
"UnionArray type_ids must be non-nullable i8, got {}",
179+
matches!(
180+
type_ids.dtype(),
181+
DType::Primitive(crate::dtype::PType::U8, _)
182+
),
183+
"UnionArray type_ids must be u8, got {}",
167184
type_ids.dtype()
168185
);
169186

@@ -178,9 +195,9 @@ impl Array<Union> {
178195
///
179196
/// # Safety
180197
///
181-
/// The caller must ensure the type IDs are non-nullable `i8` values declared by `variants`,
182-
/// every child has the corresponding variant dtype, and all arrays have the same length. The
183-
/// children must currently be non-nullable.
198+
/// The caller must ensure every non-null type ID is a `u8` value declared by `variants`, every
199+
/// child has the corresponding variant dtype, and all arrays have the same length. Null type
200+
/// IDs represent outer union nulls.
184201
pub unsafe fn new_unchecked(
185202
type_ids: ArrayRef,
186203
variants: UnionVariants,
@@ -202,9 +219,9 @@ impl Array<Union> {
202219
}
203220
}
204221

205-
/// Create an empty array for a non-nullable union dtype.
206-
pub(crate) fn empty(variants: UnionVariants) -> Self {
207-
let type_ids = PrimitiveArray::from_iter(Vec::<i8>::new()).into_array();
222+
/// Create an empty array for a union dtype.
223+
pub(crate) fn empty(variants: UnionVariants, nullability: Nullability) -> Self {
224+
let type_ids = PrimitiveArray::empty::<u8>(nullability).into_array();
208225
let children: Vec<_> = variants
209226
.variants()
210227
.map(|dtype| crate::Canonical::empty(&dtype).into_array())
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
use vortex_error::VortexResult;
5+
6+
use crate::ArrayRef;
7+
use crate::IntoArray;
8+
use crate::array::ArrayView;
9+
use crate::arrays::Union;
10+
use crate::arrays::UnionArray;
11+
use crate::arrays::union::UnionArrayExt;
12+
use crate::builtins::ArrayBuiltins;
13+
use crate::scalar_fn::fns::mask::MaskReduce;
14+
15+
impl MaskReduce for Union {
16+
fn mask(array: ArrayView<'_, Union>, mask: &ArrayRef) -> VortexResult<Option<ArrayRef>> {
17+
let type_ids = array.type_ids().clone().mask(mask.clone())?;
18+
let children = array.children();
19+
20+
// SAFETY: Masking type IDs changes only outer validity. Values at newly-null positions are
21+
// no longer selected, and every row-aligned component retains its length and dtype.
22+
Ok(Some(
23+
unsafe { UnionArray::new_unchecked(type_ids, array.variants().clone(), children) }
24+
.into_array(),
25+
))
26+
}
27+
}

vortex-array/src/arrays/union/compute/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
mod filter;
55
pub(crate) use filter::filter_union;
66

7+
mod mask;
8+
79
pub(crate) mod rules;
810

911
mod slice;

vortex-array/src/arrays/union/compute/rules.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ use crate::arrays::dict::TakeReduceAdaptor;
66
use crate::arrays::filter::FilterReduceAdaptor;
77
use crate::arrays::slice::SliceReduceAdaptor;
88
use crate::optimizer::rules::ParentRuleSet;
9+
use crate::scalar_fn::fns::mask::MaskReduceAdaptor;
910

1011
pub(crate) const PARENT_RULES: ParentRuleSet<Union> = ParentRuleSet::new(&[
1112
ParentRuleSet::lift(&SliceReduceAdaptor(Union)),
1213
ParentRuleSet::lift(&FilterReduceAdaptor(Union)),
1314
ParentRuleSet::lift(&TakeReduceAdaptor(Union)),
15+
ParentRuleSet::lift(&MaskReduceAdaptor(Union)),
1416
]);

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@
33

44
//! Canonical sparse union arrays.
55
//!
6-
//! A [`UnionArray`] stores one non-nullable `i8` type ID per row followed by one row-aligned child
7-
//! for each variant. The type ID selects which child's value is active for a row; values in all
8-
//! other children at that row are placeholders.
9-
//!
10-
//! Union nullability semantics are still being designed, so this encoding currently requires every
11-
//! variant child to be non-nullable.
6+
//! A [`UnionArray`] stores one `u8` type ID per row followed by one row-aligned child for each
7+
//! variant. The type ID selects which child's value is active for a row; values in all other
8+
//! children at that row are placeholders. Outer union nulls are stored as nulls in the type IDs
9+
//! child, independently of nulls in the selected variant child.
1210
1311
use crate::dtype::DType;
1412
use crate::dtype::Nullability;
@@ -24,7 +22,9 @@ pub(crate) mod compute;
2422
mod vtable;
2523
pub use vtable::Union;
2624

27-
pub(crate) const TYPE_IDS_DTYPE: DType = DType::Primitive(PType::I8, Nullability::NonNullable);
25+
pub(crate) fn type_ids_dtype(nullability: Nullability) -> DType {
26+
DType::Primitive(PType::U8, nullability)
27+
}
2828

2929
#[cfg(test)]
3030
mod tests;

0 commit comments

Comments
 (0)