Skip to content

Union types - #1824

Open
dmpetrov wants to merge 90 commits into
mainfrom
union
Open

Union types#1824
dmpetrov wants to merge 90 commits into
mainfrom
union

Conversation

@dmpetrov

@dmpetrov dmpetrov commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Closes #1812
Adds multi-arm Union[...] signals — Union[int, str, float], Union[ModelA, ModelB], mixed, and nullable.

Storage reuses the hidden _type_tag discriminator: _type_tag holds the active arm's name ("int", "Text"), NULL means None. Optional[Model] is just the single-arm case.

Compatibility: the old numeric _type_tag (arm index) is still read, with a FutureWarning; its removal is tracked in #1949. Mixing a legacy dataset with a new one in a single query is not supported — the discriminator column type changed Int64String. Arms are now serialized in a fixed order, so the first run after upgrading recomputes once instead of hitting its checkpoint.

Now rejected: same-shaped union arms inside a list/dict — one JSON cell has no per-element tag, so main silently reads back the wrong arm. Workaround: a distinguishing field (kind: Literal["..."]). A per-element tag could lift this later — no issue filed yet.

Out of scope (follow-up): collection arms (Union[str, list[str]]) — #1952

Adds support for multi-arm Union[...] signals (Union[basic,basic],
Union[Model,Model], mixed, nullable, and collection arms like
Union[str, list[str]]). A union stores a hidden _type_tag discriminator
plus one column-group per arm; the active arm is identified by the tag,
inactive arms are NULL/default. Optional[Model] is the single-arm case.

Includes func.variant_type(), readable arm access, and cross-backend
(SQLite + ClickHouse) round-trips with tests.
@dmpetrov
dmpetrov marked this pull request as draft June 18, 2026 16:24
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 18, 2026

Copy link
Copy Markdown

Deploying datachain with  Cloudflare Pages  Cloudflare Pages

Latest commit: c6a33fe
Status: ✅  Deploy successful!
Preview URL: https://4712f294.datachain-2g6.pages.dev
Branch Preview URL: https://union.datachain-2g6.pages.dev

View logs

@dmpetrov dmpetrov mentioned this pull request Jun 18, 2026
@codecov

codecov Bot commented Jun 18, 2026

Copy link
Copy Markdown

dmpetrov and others added 24 commits June 19, 2026 10:19
@shcheklein
shcheklein requested a review from Copilot June 28, 2026 16:32
@dmpetrov

Copy link
Copy Markdown
Contributor Author

@dreadatour thank you for the review. all fixed - please take a look.

@dreadatour

Copy link
Copy Markdown
Contributor

One more finding: a subclass of a declared union arm can be matched to a less specific arm, causing silent data loss.

from typing import Union

import datachain as dc
from datachain.lib.data_model import DataModel
from datachain.query import Session


class Animal(DataModel):
    legs: int = 4


class Zebra(Animal):
    stripes: int = 1


class BabyZebra(Zebra):
    age: int = 0


chain = dc.read_values(
    value=[BabyZebra(stripes=9, age=2)],
    output={"value": Union[Animal, Zebra]},
    session=Session.get(in_memory=True),
)

value = chain.to_values("value")[0]
print(type(value).__name__, value)

Actual:

Animal legs=4

Expected:

Zebra legs=4 stripes=9

BabyZebra matches both Animal and Zebra through isinstance(). The fallback currently selects the first matching arm in canonical type-name order, so Animal wins and the stripes field is silently discarded.

Could we select the most specific matching declared superclass and add a regression test using a subclass of Zebra?

@dmpetrov

dmpetrov commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

One more finding: a subclass of a declared union arm can be matched to a less specific arm, causing silent data loss.

Fixed: the isinstance fallback now picks the narrowest matching arm. Exact-type match still wins first, so bool vs int is unaffected.

@dreadatour dreadatour left a comment

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.

Looks good to me overall as far as I can tell — the diff is too big to be able to review it properly.

One comment before merge — let's, please, cleanup CLAUDE.md file? 😀

Comment thread pyproject.toml Outdated
"tests/unit/lib/test_signal_schema.py" = ["UP006", "UP007", "UP035", "UP045"]
"tests/unit/lib/test_optional.py" = ["UP007", "UP045"]
"tests/func/test_optional.py" = ["UP007", "UP045"]
"tests/unit/lib/test_union_types.py" = ["UP007", "UP045"]

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.

minor: can we actually fix this instead of keep growing exclusions?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done - all three entries gone, incl. N806.

Comment thread tests/unit/lib/test_union_types.py Outdated
assert arm_selector(Foo) == "Foo"


# ---- flatten / unflatten round-trips ---------------------------------------

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.

minor: cleanup style - I don't think we use sections like this (also review comments, better names, etc)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done - banners gone. Comments that restated the test name dropped. test_signal_schema_union_path_edges split into two named tests.

@dmpetrov

Copy link
Copy Markdown
Contributor Author

Thanks @shcheklein - all fixed. Plus one extra: a separate review pass caught a nested union restoring the wrong arm (the tag was written, then ignored).

I only ran this through Claude Code, no other models. Enough, or would you like a separate Codex pass? Otherwise I'd just merge it.

@shcheklein

Copy link
Copy Markdown
Contributor

@dmpetrov check the Codex output please https://claude.ai/code/artifact/345d4fe5-57b2-47fb-8579-6d5a604327be

@dmpetrov

Copy link
Copy Markdown
Contributor Author

@shcheklein thank you! Fixed 1-4, thanks. 5 and 6 are in the description now. 7 reproduces on main, leaving it out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Union type

4 participants