Skip to content

fix: keep the element type of a variadic tuple - #1963

Open
shcheklein wants to merge 4 commits into
mainfrom
fix/variadic-tuple-element-type
Open

fix: keep the element type of a variadic tuple#1963
shcheklein wants to merge 4 commits into
mainfrom
fix/variadic-tuple-element-type

Conversation

@shcheklein

@shcheklein shcheklein commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

tuple[int, ...] is stored as Array(JSON) rather than Array(Int64).

... is a real object — Ellipsis — so it turns up as an actual entry in the
type's arguments:

tuple[int, ...]   get_args -> (int, Ellipsis)
tuple[int, int]   get_args -> (int, int)
list[int]         get_args -> (int,)

_list_to_array iterated those looking for a single element type, saw int and
Ellipsis, and concluded "mixed", which is the correct answer for
tuple[int, str] and the wrong one here. So the annotation that means the same
shape as list[int] was the one that did not behave like it.

Dropping Ellipsis before the element type is read:

annotation before after
tuple[int, ...] Array(JSON) Array(Int64)
tuple[str, ...] Array(JSON) Array(String)
tuple[int | None, ...] Array(JSON) Array(Nullable(Int64))
tuple[int, str] Array(JSON) unchanged — genuinely mixed
list[int] Array(Int64) unchanged

tuple[()] names no element type at all and is refused rather than indexed
into; main reaches it as IndexError.

Compatibility

This changes the physical type of an existing annotation, so it is a storage
boundary, not only an improvement:

  • Old data still reads. A column written as Array(JSON) holds
    ["1","2"]; the read path accepts that and hydrates (1, 2) as before.
  • Comparisons across the boundary do not hold. A dataset written before this
    and one written after hold ["1","2"] and [1,2] for the same value. They
    hydrate identically, and both declare the same schema, but merge(on=...),
    distinct, subtract and grouping compare the stored text — so they disagree.
    Nothing marks which codec a column was written with.
  • An alias reading the same column shows the difference directly:
    old.mutate(copy=C("r.scores")).to_list("copy") gives ('1', '2') where a
    direct read gives (1, 2).

This is the premise for the array-codec work in
#1968: while a variadic
scalar tuple is accidentally Array(JSON), the JSON element rule cannot be made
uniform without that accident driving it.

tuple[T, ...] carries Ellipsis as its second argument to mark variable length,
but _list_to_array passed the whole argument list to list_of_args_to_type, which
tries to map each one to a SQL type. Ellipsis raises, the handler falls back to
JSON, and the declared element type is discarded:

    list[int]                   Array(Int64)
    tuple[int, ...]             Array(JSON)
    tuple[tuple[int, ...], ...] Array(JSON)

An Array(JSON) column stores each element as its own JSON document, so a tuple
of numbers was written as ["1","2"] where the same numbers in a list were
written [1,2], and a filter comparing against the natural value found nothing.

Ellipsis is dropped before the element type is inferred, so a variadic tuple
resolves to what its list counterpart resolves to. Fixed-length tuples are
unaffected: they have no Ellipsis, and a mixed one still falls back to JSON.

Datasets written before this keep their stored schema and read back unchanged.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 29, 2026

Copy link
Copy Markdown

Deploying datachain with  Cloudflare Pages  Cloudflare Pages

Latest commit: bc9fcca
Status: ✅  Deploy successful!
Preview URL: https://b8497cf8.datachain-2g6.pages.dev
Branch Preview URL: https://fix-variadic-tuple-element-t.datachain-2g6.pages.dev

View logs

@codecov

codecov Bot commented Aug 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

shcheklein added a commit that referenced this pull request Sep 1, 2026
list[Annotated[str, "meta"] | Literal[None]] raised "Cannot recognize type"
before nullability was ever considered: the item type was resolved from the
annotation as written, and neither the scalar table nor the union handling knows
that composition, though each half is recognized alone. Pydantic accepts the
annotation and ["x", None] with it.

Peel first, then resolve. Ellipsis is deliberately left in place -- it is what
marks a variadic tuple, and dropping it here would quietly turn
tuple[int, ...] into Array(Int64), which is #1963's change to make with its own
migration.

This raised no TypeError on main either, so it is a gap rather than a
regression. Declared types are unchanged across seventeen annotations and
storage across fourteen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shcheklein and others added 2 commits September 2, 2026 12:54
Stripping Ellipsis leaves tuple[()] with no argument to read, which is refused
rather than indexed into. The branch had no test, which is what codecov was
reporting; main reaches the same annotation as IndexError.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant