Skip to content

Tracking Issue: Add DType::Union #7882

Description

@connortsui20

This is a tracking issue for adding the DType::Union variant and canonical encoding for Union.

Motivation

See the parent epic issue: #7705

Design

We will be following the Arrow specification of Union, though we have an important decision to make here. Arrow specifies both a dense and a sparse union layout. Both layouts (encodings in Vortex) would have child arrays for each type that a value could be, as well as a types child array that specifies which type in the Union array a given row is (and thus, which child array to look at to fetch the correct value).

Note that we cannot reuse the StructFields type for Union as we need to also track the i8 tags for each of the possible variants of the union.

Dense vs Sparse

Dense encoding: each child array contains only the values that actually belong to that type, packed contiguously. An additional offsets buffer tells us which slot of the chosen child holds the value for that row.

Slicing and filtering requires walking type_ids to recompute per-child offsets.

type_ids: [0, 1, 0, 1, 0]
offsets:  [0, 0, 1, 1, 2]
child 0 (int):    [10, 20, 30]
child 1 (string): ["a", "b"]

Sparse encoding: every child array has the same length as the union itself, so the value at row i is simply found at position i of child type_ids[i].

The "inactive" slots of each child are unused but must still be addressable. There is no offsets buffer.

type_ids: [0, 1, 0, 1, 0]
child 0 (int):    [10, _, 20, _, 30]   // _ slots are inactive
child 1 (string): [_, "a", _, "b", _]

Why Sparse should be Canonical (and not Dense)

We want to support quick execution of things like slice and filter on our canonical arrays, and sparse is clearly superior to dense in this case (since dense would have to perform compute just to figure out how to slice child arrays).

Additionally, the space loss of sparse in Arrow does not necessarily translate to Vortex because our canonical types do not require the children to also be canonical. This means that actually sparse children under a sparse Union (for example, if I have 99000 integers and 1000 strings all separated) can be encoded with a SparseArray, so we would not lose much space compared to the dense union encoding.

It then follows that DenseUnionArray can just be a different physical encoding of Union. I think the compressor might have to be smarter about compressing into Dense though, since SparseUnion is basically always more performant for the compute we want to do.

Type IDs

Per Arrow's Schema.fbs: "By default ids in the type vector refer to the offsets in the children. Optionally typeIds provides an indirection between the child offset and the type id for each child."

So the per-row type tag defaults to consecutive 0..N matching child offsets, but an optional typeIds: [i8] array can provide indirection. For example, children [a, b, c] with typeIds = [0, 5, 7] means the data buffer uses tags 0, 5, 7 to select them (This matters for schema evolution because it means removing a child doesn't renumber the others).

UnionVariants will have to carry this information.

Also note that UnionVariants will enforce that variant names cannot be duplicated, and must be unique. The arrow specification says nothing about this, but we can enforce this behavior as the alternative of having duplicate variant names is insane.

Nullability semantics

  • Follow Arrow's row-level semantics: a union has no independent parent validity. A row is null when its selected child is null, and inactive sparse-child nulls do not matter.
  • Derive the union DType's nullability from its variants rather than storing a second outer Nullability. Implemented by Remove stored Union Nullability #8714.
  • Decide how an operation that introduces new nulls changes UnionVariants. This affects mask, take with null indices, null-producing casts, and appending a null in UnionBuilder. Until that policy is defined, these cases should return an explicit unsupported error.

Steps

Implementation history

Metadata

Metadata

Assignees

Labels

tracking-issueShared implementation context for work likely to span multiple PRs.

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions