Skip to content

Add UnionScalar#8739

Open
connortsui20 wants to merge 2 commits into
developfrom
ct/union-scalar
Open

Add UnionScalar#8739
connortsui20 wants to merge 2 commits into
developfrom
ct/union-scalar

Conversation

@connortsui20

@connortsui20 connortsui20 commented Jul 13, 2026

Copy link
Copy Markdown
Member

Rationale for this change

Tracking Issue: #7882

UnionScalar is the row representation needed for scalar access and constant arrays before the canonical UnionArray. Non-null values store the selected type ID and child ScalarValue; null remains on the outer Scalar, avoiding a second null layer.

What changes are included in this PR?

  • add UnionValue, UnionScalar, construction, downcasts, validation, display, equality, hashing, defaults, and sizing
  • add scalar protobuf round trips and keep non-identity Union casts unsupported
  • add arbitrary generation and ConstantArray scalar access coverage

Tested with cargo nextest run -p vortex-array and cargo clippy --all-targets --all-features.

What APIs are changed? Are there any user-facing changes?

Adds ScalarValue::Union, UnionValue, UnionScalar, Scalar::union, and the Union scalar downcasts.

@codspeed-hq

codspeed-hq Bot commented Jul 13, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 1660 untouched benchmarks
⏩ 47 skipped benchmarks1


Comparing ct/union-scalar (1e93238) with develop (8ea5dcd)

Open in CodSpeed

Footnotes

  1. 47 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@connortsui20 connortsui20 changed the title Add Union scalar support Add UnionScalar Jul 13, 2026
@connortsui20 connortsui20 force-pushed the ct/union-scalar branch 4 times, most recently from 7e619d3 to 243969b Compare July 13, 2026 16:08
Comment on lines +27 to +35
// Union nullability is part of its variant dtypes, so the generic nullability-only cast
// below is not valid for unions. Keep all non-identity union casts unsupported until their
// semantics are defined.
if matches!(self.dtype(), DType::Union(_)) || matches!(target_dtype, DType::Union(_)) {
vortex_bail!(
"non-identity union scalar cast from {} to {target_dtype} is not supported",
self.dtype()
);
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interested in what people think about how we should deal with this

@connortsui20 connortsui20 force-pushed the ct/union-scalar branch 3 times, most recently from 8bb8db6 to 7925944 Compare July 13, 2026 19:41
/// We implement `Hash` manually so top-level dtype nullability does not affect the hash.
impl Hash for Scalar {
fn hash<H: Hasher>(&self, state: &mut H) {
// TODO(connor): Recursively ignore dtype nullability to match Scalar equality (#8744).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@connortsui20 connortsui20 force-pushed the ct/union-scalar branch 3 times, most recently from 43d2fc9 to 9de7663 Compare July 13, 2026 20:01
@connortsui20 connortsui20 added the changelog/feature A new feature label Jul 13, 2026
Base automatically changed from codex/reject-negative-union-type-ids to develop July 13, 2026 20:34
@connortsui20 connortsui20 marked this pull request as ready for review July 13, 2026 22:05
Comment thread vortex-array/src/scalar/typed_view/union.rs Outdated
// Union nullability is part of its variant dtypes, so the generic nullability-only cast
// below is not valid for unions. Keep all non-identity union casts unsupported until their
// semantics are defined.
if self.dtype().is_union() || target_dtype.is_union() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you have to gate the other branch and make sure this falls back to union scalar

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the other branch?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the one the comment refers to

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I understand, are you saying that it can somehow get past this gate?

Comment thread vortex-array/src/scalar/cast.rs Outdated
DType::List(..) | DType::FixedSizeList(..) => self.as_list().cast(target_dtype),
DType::Struct(..) => self.as_struct().cast(target_dtype),
DType::Union(..) => todo!("TODO(connor)[Union]: unimplemented"),
DType::Union(..) => unreachable!("union casts are handled before scalar dispatch"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@connortsui20 I am saying you should fallback here in your cast

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't really understand here, what exactly do you mean by "falls back to union scalar"? We already have a union scalar in the input

}

/// Returns the non-null zero value for `dtype`, or [`None`] if no such value exists.
fn try_zero_value(dtype: &DType) -> Option<Self> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get why this needs to return option, Null is a weird type

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Null isn't the only type that returns None here though (see FSL, Struct, and now Union)? This just improves the error message in general

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because null returns none, I think the develop behaviour is better

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is just an implementation detail though? this is a private helper so the behavior is the same (just different error message)?

Self::Tuple(field_values)
}
DType::Union(..) => todo!("TODO(connor)[Union]: unimplemented"),
DType::Union(_) => vortex_panic!("{dtype} has no default value"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could pick first variant

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its not clear what to do if there are no variants (empty union / void type)

@robert3005

Copy link
Copy Markdown
Contributor

I think as_nullable for union has to traverse all the children and make them nullable. Since there's no top level nullability there's no other implementation that makes sense imho

Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
@connortsui20

Copy link
Copy Markdown
Member Author

This does mean that as_nullable will have different behavior than struct for example which only does the top-level thing, but lets just do it and see how well that works

A union has no top-level nullability of its own; its nullability is
derived from its variants. So DType::with_nullability on a union now
propagates the requested nullability into every variant instead of being
a no-op, via a new UnionVariants::with_nullability helper.

This also lets Scalar::into_nullable work on non-nullable unions (it
previously had to assert against the no-nullable-representation case), so
the assertion and its panic path are removed.

Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@robert3005

Copy link
Copy Markdown
Contributor

why is it different? Struct has top level nullability while the union doesn't

@connortsui20 connortsui20 requested a review from robert3005 July 14, 2026 16:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/feature A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants