Skip to content

Commit 4893177

Browse files
committed
Add Union scalar support
Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent 5989962 commit 4893177

17 files changed

Lines changed: 963 additions & 25 deletions

File tree

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,46 @@ impl OperationsVTable<Constant> for Constant {
1818
Ok(array.scalar.clone())
1919
}
2020
}
21+
22+
#[cfg(test)]
23+
mod tests {
24+
use vortex_error::VortexResult;
25+
26+
use crate::IntoArray;
27+
use crate::VortexSessionExecute;
28+
use crate::arrays::ConstantArray;
29+
use crate::dtype::DType;
30+
use crate::dtype::Nullability;
31+
use crate::dtype::PType;
32+
use crate::dtype::UnionVariants;
33+
use crate::scalar::Scalar;
34+
35+
#[test]
36+
fn scalar_at_preserves_union_scalar() -> VortexResult<()> {
37+
let variants = UnionVariants::try_new(
38+
["int", "string"].into(),
39+
vec![
40+
DType::Primitive(PType::I32, Nullability::Nullable),
41+
DType::Utf8(Nullability::NonNullable),
42+
],
43+
vec![5, 9],
44+
)?;
45+
46+
let scalar = Scalar::union(
47+
variants.clone(),
48+
5,
49+
Scalar::primitive(42_i32, Nullability::Nullable),
50+
)?;
51+
let array = ConstantArray::new(scalar.clone(), 3).into_array();
52+
let mut ctx = crate::array_session().create_execution_ctx();
53+
54+
assert_eq!(array.execute_scalar(1, &mut ctx)?, scalar);
55+
56+
let null = Scalar::null(DType::Union(variants));
57+
let array = ConstantArray::new(null.clone(), 3).into_array();
58+
59+
assert_eq!(array.execute_scalar(1, &mut ctx)?, null);
60+
61+
Ok(())
62+
}
63+
}

vortex-array/src/scalar/arbitrary.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,21 @@ pub fn random_scalar(u: &mut Unstructured, dtype: &DType) -> Result<Scalar> {
9595
)),
9696
)
9797
.vortex_expect("unable to construct random `Scalar`_"),
98-
DType::Union(..) => todo!("TODO(connor)[Union]: unimplemented"),
98+
DType::Union(variants) => {
99+
let child_index = u.choose_index(variants.len())?;
100+
101+
let child_dtype = variants
102+
.variant_by_index(child_index)
103+
.vortex_expect("chosen union child index must be valid");
104+
let child = random_scalar(u, &child_dtype)?;
105+
106+
Scalar::union(
107+
variants.clone(),
108+
variants.child_index_to_tag(child_index),
109+
child,
110+
)
111+
.vortex_expect("generated union scalar must be valid")
112+
}
99113
DType::Variant(_) => todo!(),
100114
DType::Extension(..) => {
101115
unreachable!("Can't yet generate arbitrary scalars for ext dtype")

vortex-array/src/scalar/cast.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ impl Scalar {
2424
return Ok(self.clone());
2525
}
2626

27+
// Union nullability is part of its variant dtypes, so the generic nullability-only cast
28+
// below is not valid for unions. Keep all non-identity union casts unsupported until their
29+
// semantics are defined.
30+
if self.dtype().is_union() || target_dtype.is_union() {
31+
vortex_bail!(
32+
"non-identity union scalar cast from {} to {target_dtype} is not supported",
33+
self.dtype()
34+
);
35+
}
36+
2737
// Check for solely nullability casting.
2838
if self.dtype().eq_ignore_nullability(target_dtype) {
2939
// Cast from non-nullable to nullable or vice versa.
@@ -58,16 +68,27 @@ impl Scalar {
5868
DType::Binary(_) => self.as_binary().cast(target_dtype),
5969
DType::List(..) | DType::FixedSizeList(..) => self.as_list().cast(target_dtype),
6070
DType::Struct(..) => self.as_struct().cast(target_dtype),
61-
DType::Union(..) => todo!("TODO(connor)[Union]: unimplemented"),
71+
DType::Union(..) => unreachable!("union casts are handled before scalar dispatch"),
6272
DType::Variant(_) => vortex_bail!("Variant scalars can't be cast to {target_dtype}"),
6373
DType::Extension(..) => self.as_extension().cast(target_dtype),
6474
}
6575
}
6676

6777
/// Cast the scalar into a nullable version of its current type.
78+
///
79+
/// # Panics
80+
///
81+
/// Panics if the scalar's dtype has no nullable representation, such as a union dtype when all
82+
/// of its variants are non-nullable.
6883
pub fn into_nullable(self) -> Scalar {
6984
let (dtype, value) = self.into_parts();
70-
Self::try_new(dtype.as_nullable(), value)
85+
let nullable_dtype = dtype.as_nullable();
86+
assert!(
87+
nullable_dtype.is_nullable(),
88+
"dtype {dtype} cannot be made nullable"
89+
);
90+
91+
Self::try_new(nullable_dtype, value)
7192
.vortex_expect("Casting to nullable should always succeed")
7293
}
7394
}

vortex-array/src/scalar/constructor.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,25 @@ use std::sync::Arc;
88
use vortex_buffer::BufferString;
99
use vortex_buffer::ByteBuffer;
1010
use vortex_error::VortexExpect;
11+
use vortex_error::VortexResult;
12+
use vortex_error::vortex_ensure_eq;
13+
use vortex_error::vortex_err;
1114
use vortex_error::vortex_panic;
1215

1316
use crate::dtype::DType;
1417
use crate::dtype::DecimalDType;
1518
use crate::dtype::NativePType;
1619
use crate::dtype::Nullability;
1720
use crate::dtype::PType;
21+
use crate::dtype::UnionVariants;
1822
use crate::dtype::extension::ExtDType;
1923
use crate::dtype::extension::ExtDTypeRef;
2024
use crate::dtype::extension::ExtVTable;
2125
use crate::scalar::DecimalValue;
2226
use crate::scalar::PValue;
2327
use crate::scalar::Scalar;
2428
use crate::scalar::ScalarValue;
29+
use crate::scalar::UnionValue;
2530

2631
// TODO(connor): Really, we want `try_` constructors that return errors instead of just panic.
2732
impl Scalar {
@@ -190,6 +195,42 @@ impl Scalar {
190195
.vortex_expect("unable to construct an extension `Scalar`")
191196
}
192197

198+
/// Creates a union scalar from a type ID and child scalar.
199+
///
200+
/// A null child is normalized to a null union scalar, so null union scalars do not retain a
201+
/// selected type ID. A type ID is not part of a null union scalar's logical identity and is not
202+
/// preserved across serialization or other round trips. The type ID and exact child dtype are
203+
/// still validated before normalization.
204+
///
205+
/// # Errors
206+
///
207+
/// Returns an error if the type ID is not present in `variants` or if the child scalar's dtype
208+
/// does not exactly match the selected variant dtype.
209+
pub fn union(variants: UnionVariants, type_id: i8, child: Scalar) -> VortexResult<Self> {
210+
let child_index = variants.tag_to_child_index(type_id).ok_or_else(|| {
211+
vortex_err!(
212+
"union type ID {type_id} is not present in {:?}",
213+
variants.type_ids()
214+
)
215+
})?;
216+
217+
let expected_dtype = variants
218+
.variant_by_index(child_index)
219+
.vortex_expect("type ID resolved to a valid child index");
220+
221+
vortex_ensure_eq!(
222+
child.dtype(),
223+
&expected_dtype,
224+
"union type ID {type_id} selects child dtype {expected_dtype}, got {}",
225+
child.dtype()
226+
);
227+
228+
let (_, child_value) = child.into_parts();
229+
let value = child_value.map(|value| ScalarValue::Union(UnionValue::new(type_id, value)));
230+
231+
Self::try_new(DType::Union(variants), value)
232+
}
233+
193234
/// Creates a new variant scalar from a row-specific nested scalar.
194235
///
195236
/// Use [`Scalar::null(DType::Variant(Nullability::Nullable))`][Scalar::null] for a top-level

vortex-array/src/scalar/display.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ impl Display for Scalar {
2020
DType::Binary(_) => write!(f, "{}", self.as_binary()),
2121
DType::List(..) | DType::FixedSizeList(..) => write!(f, "{}", self.as_list()),
2222
DType::Struct(..) => write!(f, "{}", self.as_struct()),
23-
DType::Union(..) => todo!("TODO(connor)[Union]: unimplemented"),
23+
DType::Union(..) => write!(f, "{}", self.as_union()),
2424
DType::Variant(_) => write!(f, "{}", self.as_variant()),
2525
DType::Extension(_) => write!(f, "{}", self.as_extension()),
2626
}
@@ -30,13 +30,15 @@ impl Display for Scalar {
3030
#[cfg(test)]
3131
mod tests {
3232
use vortex_buffer::ByteBuffer;
33+
use vortex_error::VortexResult;
3334

3435
use crate::dtype::DType;
3536
use crate::dtype::FieldName;
3637
use crate::dtype::Nullability::NonNullable;
3738
use crate::dtype::Nullability::Nullable;
3839
use crate::dtype::PType;
3940
use crate::dtype::StructFields;
41+
use crate::dtype::UnionVariants;
4042
use crate::extension::datetime::Date;
4143
use crate::extension::datetime::Time;
4244
use crate::extension::datetime::TimeUnit;
@@ -79,6 +81,24 @@ mod tests {
7981
);
8082
}
8183

84+
#[test]
85+
fn display_union() -> VortexResult<()> {
86+
let variants = UnionVariants::new(
87+
["int", "string"].into(),
88+
vec![
89+
DType::Primitive(PType::I32, Nullable),
90+
DType::Utf8(NonNullable),
91+
],
92+
)?;
93+
94+
let scalar = Scalar::union(variants.clone(), 0, Scalar::primitive(42_i32, Nullable))?;
95+
96+
assert_eq!(format!("{scalar}"), "int(42i32)");
97+
assert_eq!(format!("{}", Scalar::null(DType::Union(variants))), "null");
98+
99+
Ok(())
100+
}
101+
82102
#[test]
83103
fn display_utf8() {
84104
assert_eq!(

vortex-array/src/scalar/downcast.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use vortex_buffer::ByteBuffer;
88
use vortex_error::VortexExpect;
99
use vortex_error::vortex_panic;
1010

11+
use crate::dtype::DType;
1112
use crate::scalar::BinaryScalar;
1213
use crate::scalar::BoolScalar;
1314
use crate::scalar::DecimalScalar;
@@ -19,6 +20,8 @@ use crate::scalar::PrimitiveScalar;
1920
use crate::scalar::Scalar;
2021
use crate::scalar::ScalarValue;
2122
use crate::scalar::StructScalar;
23+
use crate::scalar::UnionScalar;
24+
use crate::scalar::UnionValue;
2225
use crate::scalar::Utf8Scalar;
2326
use crate::scalar::VariantScalar;
2427

@@ -156,6 +159,26 @@ impl Scalar {
156159
Some(ExtScalar::new_unchecked(self.dtype(), self.value()))
157160
}
158161

162+
/// Returns a view of the scalar as a union scalar.
163+
///
164+
/// # Panics
165+
///
166+
/// Panics if the scalar does not have a [`Union`](crate::dtype::DType::Union) type.
167+
pub fn as_union(&self) -> UnionScalar<'_> {
168+
self.as_union_opt()
169+
.vortex_expect("Failed to convert scalar to union")
170+
}
171+
172+
/// Returns a view of the scalar as a union scalar if it has a union type.
173+
pub fn as_union_opt(&self) -> Option<UnionScalar<'_>> {
174+
if !matches!(self.dtype(), DType::Union(_)) {
175+
return None;
176+
}
177+
178+
// Scalar construction has already validated the value against this union dtype.
179+
Some(UnionScalar::new_unchecked(self.dtype(), self.value()))
180+
}
181+
159182
/// Returns a view of the scalar as a variant scalar.
160183
///
161184
/// # Panics
@@ -273,6 +296,22 @@ impl ScalarValue {
273296
}
274297
}
275298

299+
/// Returns the union value, panicking if the value is not a [`Union`](ScalarValue::Union).
300+
pub fn as_union(&self) -> &UnionValue {
301+
match self {
302+
ScalarValue::Union(value) => value,
303+
_ => vortex_panic!("ScalarValue is not a Union"),
304+
}
305+
}
306+
307+
/// Returns the union value, panicking if the value is not a [`Union`](ScalarValue::Union).
308+
pub fn into_union(self) -> UnionValue {
309+
match self {
310+
ScalarValue::Union(value) => value,
311+
_ => vortex_panic!("ScalarValue is not a Union"),
312+
}
313+
}
314+
276315
/// Returns the row-specific scalar wrapped by a variant, panicking if the value is not a
277316
/// variant.
278317
pub fn as_variant(&self) -> &Scalar {

vortex-array/src/scalar/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
//! Scalar values and types for the Vortex system.
55
//!
66
//! This crate provides scalar types and values that can be used to represent individual data
7-
//! elements in the Vortex array system. [`Scalar`]s are composed of a logical data type ([`DType`])
8-
//! and an optional (encoding nullability) value ([`ScalarValue`]).
7+
//! elements in the Vortex array system. A [`Scalar`] pairs a logical data type ([`DType`]) with an
8+
//! optional non-null value ([`ScalarValue`]); [`None`] represents a null scalar.
99
//!
1010
//! Note that the implementations of `Scalar` are split into several different modules.
1111
//!

0 commit comments

Comments
 (0)