Skip to content

Commit c5f26f5

Browse files
committed
Add outer nullability to Union dtypes
Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent 17f843a commit c5f26f5

16 files changed

Lines changed: 163 additions & 113 deletions

File tree

‎vortex-array/src/aggregate_fn/fns/uncompressed_size_in_bytes/mod.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ fn supports_uncompressed_size_in_bytes(dtype: &DType) -> bool {
289289
DType::Struct(fields, _) => fields
290290
.fields()
291291
.all(|field| supports_uncompressed_size_in_bytes(&field)),
292-
DType::Union(variants) => variants
292+
DType::Union(variants, _) => variants
293293
.variants()
294294
.all(|variant| supports_uncompressed_size_in_bytes(&variant)),
295295
DType::Variant(_) => false,

‎vortex-array/src/dtype/coercion.rs‎

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -80,20 +80,14 @@ impl DType {
8080
/// Returns `None` if no common supertype exists.
8181
pub fn least_supertype(&self, other: &DType) -> Option<DType> {
8282
match (self, other) {
83-
(DType::Union(lhs), DType::Union(rhs)) => {
84-
return (lhs == rhs).then(|| self.clone());
83+
(DType::Union(lhs, lhs_null), DType::Union(rhs, rhs_null)) => {
84+
return (lhs == rhs).then(|| DType::Union(lhs.clone(), *lhs_null | *rhs_null));
8585
}
86-
(DType::Null, DType::Union(variants)) => {
87-
return variants
88-
.derived_nullability()
89-
.is_nullable()
90-
.then(|| other.clone());
86+
(DType::Null, DType::Union(..)) => {
87+
return Some(other.as_nullable());
9188
}
92-
(DType::Union(variants), DType::Null) => {
93-
return variants
94-
.derived_nullability()
95-
.is_nullable()
96-
.then(|| self.clone());
89+
(DType::Union(..), DType::Null) => {
90+
return Some(self.as_nullable());
9791
}
9892
_ => {}
9993
}
@@ -377,13 +371,15 @@ mod tests {
377371
vec![DType::Primitive(PType::I32, NonNullable)],
378372
)
379373
.unwrap(),
374+
NonNullable,
380375
);
381376
let nullable = DType::Union(
382377
UnionVariants::new(
383378
["value"].into(),
384379
vec![DType::Primitive(PType::I32, Nullable)],
385380
)
386381
.unwrap(),
382+
NonNullable,
387383
);
388384

389385
assert_eq!(
@@ -395,29 +391,22 @@ mod tests {
395391
}
396392

397393
#[test]
398-
fn least_supertype_null_requires_nullable_union() {
394+
fn least_supertype_null_makes_union_outer_nullable() {
399395
let nonnullable = DType::Union(
400396
UnionVariants::new(
401397
["value"].into(),
402398
vec![DType::Primitive(PType::I32, NonNullable)],
403399
)
404400
.unwrap(),
401+
NonNullable,
405402
);
406-
let nullable = DType::Union(
407-
UnionVariants::new(
408-
["value"].into(),
409-
vec![DType::Primitive(PType::I32, Nullable)],
410-
)
411-
.unwrap(),
412-
);
403+
let nullable = nonnullable.as_nullable();
413404

414-
assert!(DType::Null.least_supertype(&nonnullable).is_none());
415-
assert!(nonnullable.least_supertype(&DType::Null).is_none());
416405
assert_eq!(
417-
DType::Null.least_supertype(&nullable),
406+
DType::Null.least_supertype(&nonnullable),
418407
Some(nullable.clone())
419408
);
420-
assert_eq!(nullable.least_supertype(&DType::Null), Some(nullable));
409+
assert_eq!(nonnullable.least_supertype(&DType::Null), Some(nullable));
421410
}
422411

423412
#[test]

‎vortex-array/src/dtype/dtype_impl.rs‎

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ impl DType {
6464
| List(_, null)
6565
| FixedSizeList(_, _, null)
6666
| Struct(_, null)
67+
| Union(_, null)
6768
| Variant(null) => matches!(null, Nullability::Nullable),
68-
Union(variants) => variants.derived_nullability().is_nullable(),
6969
Extension(ext_dtype) => ext_dtype.storage_dtype().is_nullable(),
7070
}
7171
}
@@ -82,8 +82,7 @@ impl DType {
8282

8383
/// Get a new DType with the given nullability (but otherwise the same as `self`).
8484
///
85-
/// [`DType::Null`] and [`DType::Union`] have intrinsic nullability and are returned unchanged.
86-
/// To change a union's nullability, construct different [`UnionVariants`].
85+
/// [`DType::Null`] has intrinsic nullability and is returned unchanged.
8786
pub fn with_nullability(&self, nullability: Nullability) -> Self {
8887
match self {
8988
Null => Null,
@@ -95,7 +94,7 @@ impl DType {
9594
List(edt, _) => List(Arc::clone(edt), nullability),
9695
FixedSizeList(edt, size, _) => FixedSizeList(Arc::clone(edt), *size, nullability),
9796
Struct(sf, _) => Struct(sf.clone(), nullability),
98-
Union(vs) => Union(vs.clone()),
97+
Union(vs, _) => Union(vs.clone(), nullability),
9998
Variant(_) => Variant(nullability),
10099
Extension(ext) => Extension(ext.with_nullability(nullability)),
101100
}
@@ -127,7 +126,7 @@ impl DType {
127126
.zip_eq(rhs_dtype.fields())
128127
.all(|(l, r)| l.eq_ignore_nullability(&r)))
129128
}
130-
(Union(lhs), Union(rhs)) => {
129+
(Union(lhs, _), Union(rhs, _)) => {
131130
// Equal `names` implies equal length by FieldNames equality.
132131
lhs.names() == rhs.names()
133132
&& lhs.type_ids() == rhs.type_ids()
@@ -439,12 +438,20 @@ impl DType {
439438

440439
/// Get the [`UnionVariants`] if `self` is a [`DType::Union`], otherwise `None`.
441440
pub fn as_union_variants_opt(&self) -> Option<&UnionVariants> {
442-
if let Union(uv) = self { Some(uv) } else { None }
441+
if let Union(uv, _) = self {
442+
Some(uv)
443+
} else {
444+
None
445+
}
443446
}
444447

445448
/// Owned version of [Self::as_union_variants_opt].
446449
pub fn into_union_variants_opt(self) -> Option<UnionVariants> {
447-
if let Union(uv) = self { Some(uv) } else { None }
450+
if let Union(uv, _) = self {
451+
Some(uv)
452+
} else {
453+
None
454+
}
448455
}
449456

450457
/// Downcast a `DType` to an `ExtDType`
@@ -498,7 +505,7 @@ impl Display for DType {
498505
.map(|(field_null, dt)| format!("{field_null}={dt}"))
499506
.join(", "),
500507
),
501-
Union(uv) => write!(f, "union({uv}){}", uv.derived_nullability()),
508+
Union(uv, null) => write!(f, "union({uv}){null}"),
502509
Variant(null) => write!(f, "variant{null}"),
503510
Extension(ext) => write!(f, "{}", ext),
504511
}

‎vortex-array/src/dtype/mod.rs‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,15 +111,13 @@ pub enum DType {
111111
/// A logical union (sum) type.
112112
///
113113
/// A `Union` is composed of one or more **variants**, each with a name and a `DType`. A per-row
114-
/// `i8` tag selects which variant is "live" at that row.
114+
/// `u8` tag selects which variant is "live" at that row.
115115
///
116-
/// Unlike other nested types, a union has no independent outer nullability. Its nullability is
117-
/// derived at runtime from its variants: a union is nullable when any variant can contain null.
118-
/// A concrete union array has no parent validity bitmap; a row's validity is the validity of
119-
/// its selected child.
116+
/// A union has independent outer nullability in addition to the nullability of each variant.
117+
/// This distinguishes a null union from a non-null union whose selected child is null.
120118
///
121119
/// See [`UnionVariants`] for the type-tag conventions and accessors.
122-
Union(UnionVariants),
120+
Union(UnionVariants, Nullability),
123121

124122
/// Dynamically typed values stored as Vortex scalars.
125123
///
@@ -152,7 +150,7 @@ impl PartialEq for DType {
152150
// StructFields handles its own Arc::ptr_eq in its PartialEq impl.
153151
(Self::Struct(a, na), Self::Struct(b, nb)) => na == nb && a == b,
154152
// UnionVariants handles its own Arc::ptr_eq in its PartialEq impl.
155-
(Self::Union(a), Self::Union(b)) => a == b,
153+
(Self::Union(a, na), Self::Union(b, nb)) => na == nb && a == b,
156154
(Self::Variant(a), Self::Variant(b)) => a == b,
157155
(Self::Extension(a), Self::Extension(b)) => a == b,
158156
// Every variant is listed in the first position so that adding a new

‎vortex-array/src/dtype/serde/flatbuffers.rs‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ impl TryFrom<ViewedDType> for DType {
235235
.ok_or_else(|| vortex_err!("failed to parse union from flatbuffer"))?;
236236
let variants =
237237
UnionVariants::from_fb(fb_union, vfdt.buffer().clone(), vfdt.session.clone())?;
238-
Ok(Self::Union(variants))
238+
Ok(Self::Union(variants, fb_union.nullable().into()))
239239
}
240240
fb::Type::Variant => {
241241
let fb_variant = fb
@@ -381,7 +381,7 @@ impl WriteFlatBuffer for DType {
381381
)
382382
.as_union_value()
383383
}
384-
Self::Union(uv) => {
384+
Self::Union(uv, n) => {
385385
let names = uv
386386
.names()
387387
.iter()
@@ -411,6 +411,7 @@ impl WriteFlatBuffer for DType {
411411
names,
412412
dtypes,
413413
type_ids,
414+
nullable: (*n).into(),
414415
},
415416
)
416417
.as_union_value()
@@ -581,6 +582,7 @@ mod test {
581582
],
582583
)
583584
.unwrap(),
585+
Nullability::NonNullable,
584586
)
585587
}
586588

@@ -597,6 +599,7 @@ mod test {
597599
vec![DType::Null, DType::Utf8(Nullability::NonNullable)],
598600
)
599601
.unwrap(),
602+
Nullability::NonNullable,
600603
);
601604
roundtrip_dtype(dtype);
602605
}
@@ -614,6 +617,7 @@ mod test {
614617
vec![0, 5, u8::MAX],
615618
)
616619
.unwrap(),
620+
Nullability::Nullable,
617621
);
618622

619623
let bytes = dtype.write_flatbuffer_bytes().unwrap();
@@ -626,7 +630,7 @@ mod test {
626630

627631
let deserialized = DType::try_from(view).unwrap();
628632
assert_eq!(dtype, deserialized);
629-
let DType::Union(uv) = &deserialized else {
633+
let DType::Union(uv, _) = &deserialized else {
630634
panic!("Expected Union");
631635
};
632636
assert_eq!(uv.type_ids(), &[0, 5, u8::MAX]);
@@ -649,6 +653,7 @@ mod test {
649653
vec![DType::Utf8(Nullability::NonNullable), struct_with_union],
650654
)
651655
.unwrap(),
656+
Nullability::Nullable,
652657
);
653658

654659
roundtrip_dtype(outer_union);
@@ -704,6 +709,7 @@ mod test {
704709
names: Some(names),
705710
dtypes: Some(dtypes),
706711
type_ids: Some(type_ids),
712+
nullable: false,
707713
},
708714
);
709715

‎vortex-array/src/dtype/serde/mod.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,12 +187,13 @@ mod test {
187187
fn test_serde_union_dtype_json_roundtrip() {
188188
let dtype = DType::Union(
189189
UnionVariants::new(["value"].into(), vec![DType::Utf8(Nullability::Nullable)]).unwrap(),
190+
Nullability::Nullable,
190191
);
191192

192193
let json = serde_json::to_string(&dtype).unwrap();
193194
assert_eq!(
194195
json,
195-
r#"{"Union":{"names":["value"],"dtypes":[{"Utf8":true}],"type_ids":[0]}}"#
196+
r#"{"Union":[{"names":["value"],"dtypes":[{"Utf8":true}],"type_ids":[0]},true]}"#
196197
);
197198
let mut deserializer = serde_json::Deserializer::from_str(&json);
198199
let deserialized: DType = DTypeSerde::<DType>::new(&SESSION)

‎vortex-array/src/dtype/serde/proto.rs‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ impl DType {
104104
})
105105
.collect::<VortexResult<Vec<_>>>()?;
106106
let variants = UnionVariants::try_new(names, dtypes, type_ids)?;
107-
Ok(Self::Union(variants))
107+
Ok(Self::Union(variants, u.nullable.into()))
108108
}
109109
DtypeType::Variant(v) => Ok(Self::Variant(v.nullable.into())),
110110
DtypeType::Extension(e) => {
@@ -173,13 +173,14 @@ impl TryFrom<&DType> for pb::DType {
173173
.collect::<VortexResult<Vec<_>>>()?,
174174
nullable: (*null).into(),
175175
}),
176-
DType::Union(uv) => DtypeType::Union(pb::Union {
176+
DType::Union(uv, null) => DtypeType::Union(pb::Union {
177177
names: uv.names().iter().map(|n| n.as_ref().to_string()).collect(),
178178
dtypes: uv
179179
.variants()
180180
.map(|d| Self::try_from(&d))
181181
.collect::<VortexResult<Vec<_>>>()?,
182182
type_ids: uv.type_ids().iter().map(|t| *t as i32).collect(),
183+
nullable: (*null).into(),
183184
}),
184185
DType::Variant(null) => DtypeType::Variant(pb::Variant {
185186
nullable: (*null).into(),
@@ -416,22 +417,20 @@ mod tests {
416417
],
417418
)
418419
.unwrap();
419-
let dtype = DType::Union(variants);
420+
let dtype = DType::Union(variants, Nullability::NonNullable);
420421
let converted = round_trip_dtype(&dtype);
421422
assert_eq!(dtype, converted);
422423
}
423424

424425
#[test]
425-
fn test_union_round_trip_proto_with_nullable_variant() {
426+
fn test_union_round_trip_proto_with_inner_and_outer_nullability() {
426427
let variants = UnionVariants::new(
427428
["null_variant", "str"].into(),
428429
vec![DType::Null, DType::Utf8(Nullability::NonNullable)],
429430
)
430431
.unwrap();
431432

432-
assert_eq!(variants.derived_nullability(), Nullability::Nullable);
433-
434-
let dtype = DType::Union(variants);
433+
let dtype = DType::Union(variants, Nullability::Nullable);
435434
let converted = round_trip_dtype(&dtype);
436435
assert_eq!(dtype, converted);
437436
}
@@ -449,11 +448,11 @@ mod tests {
449448
)
450449
.unwrap();
451450

452-
let dtype = DType::Union(variants);
451+
let dtype = DType::Union(variants, Nullability::Nullable);
453452
let converted = round_trip_dtype(&dtype);
454453
assert_eq!(dtype, converted);
455454

456-
let DType::Union(uv) = &converted else {
455+
let DType::Union(uv, _) = &converted else {
457456
panic!("Expected Union");
458457
};
459458
assert_eq!(uv.type_ids(), &[0, 5, 7]);
@@ -471,6 +470,7 @@ mod tests {
471470
],
472471
// 256 does not fit in u8.
473472
type_ids: vec![256],
473+
nullable: false,
474474
})),
475475
};
476476

@@ -495,6 +495,7 @@ mod tests {
495495
],
496496
)
497497
.unwrap(),
498+
Nullability::NonNullable,
498499
);
499500

500501
let struct_with_union = DType::Struct(
@@ -511,6 +512,7 @@ mod tests {
511512
vec![DType::Utf8(Nullability::NonNullable), struct_with_union],
512513
)
513514
.unwrap(),
515+
Nullability::Nullable,
514516
);
515517

516518
let converted = round_trip_dtype(&outer_union);

0 commit comments

Comments
 (0)