fix: flatten models in tuples and nested mapping values as lists already are - #1960
fix: flatten models in tuples and nested mapping values as lists already are#1960shcheklein wants to merge 1 commit into
Conversation
Deploying datachain with
|
| Latest commit: |
eb04914
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://3e34f83a.datachain-2g6.pages.dev |
| Branch Preview URL: | https://fix-flatten-tuple-of-models.datachain-2g6.pages.dev |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
Aligns tuple and nested mapping model serialization with existing list behavior.
Changes:
- Flattens models inside tuples and nested mapping values.
- Adds flattening and persistence round-trip tests.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/datachain/lib/convert/flatten.py |
Adds tuple and recursive mapping flattening. |
tests/unit/lib/test_signal_schema.py |
Tests collection flattening shapes. |
tests/unit/lib/test_datachain.py |
Tests save/read round trips. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
1c48020 to
7e397ed
Compare
|
Closing this. The review found that changing the physical That is the same failure I documented in #1943 as the storage-convergence dead end, and I checked the wrong half of it here: I verified old data still reads, not that old and new rows still compare. Two further findings, both confirmed:
The one piece worth keeping is annotation-guided traversal: deciding what to descend into from the declared type rather than The |
7e397ed to
ed1c877
Compare
…ady are
_flatten_fields_values decided what to convert from the runtime type of a
field's value and knew only list and dict. A tuple matched neither, and the dict
branch converted a value that was itself a model but not one that was a list of
them, so those reached the warehouse as live pydantic instances while the same
models in a list arrived as plain dicts. The warehouse then serialized each
instance on its own, writing a tuple of models as an array of JSON strings where
a list of the same models is an array of objects:
list[Item] [{"n":1,"label":"a"}]
tuple[Item, ...] ["{\"n\":1,\"label\":\"a\"}"]
A dataset written before this still reads; the two now agree on what they write.
Models are converted with model_dump(mode="json"), which is what the warehouse
applies to a model handed to it directly. The list path used python mode, which
converts nothing, so a pathlib.Path field failed to serialize and a serializer
declared with when_used="json" was skipped -- storing the raw value a redacting
serializer existed to hide. Both now behave the same wherever the model sits.
What to walk comes from the declared type rather than the value. A collection
that cannot hold a model is returned untouched, so a vector of numbers is not
copied and keeps its identity for content hashing, and this holds per element:
tuple[Item, list[int]] converts the model and leaves the list alone. An erased
annotation -- Any, object, a bare container -- says nothing about its contents,
so it is walked rather than assumed model-free. Elements are converted one at a
time rather than from whatever the first one happens to be, which a fixed-length
tuple needs: tuple[Item, int] would otherwise call model_dump on the int.
tuple[int, Model] is still written as an array of strings, unchanged. The
warehouse picks the array conversion from the first element, so a leading
non-model sends the whole array down the per-element path.
ed1c877 to
eb04914
Compare
|
Superseded. The tuple work is being reconsidered on top of #1943 rather than as a format change in its own right, and the diagnosis moved: the double-encoding of Findings from this PR worth keeping for whatever replaces it: unwrap |
Models stored in a
tupleare written double-encoded, unlike the same models in alist.What lands in the column:
Why
_flatten_fields_valuesdecided what to convert from the runtime type of the value and knew onlylistanddict. A tuple matched neither, so it was passed through with itsBoxobjects intact and the warehouse serialized each one separately. Thedictbranch had the same gap one level down: it converted a value that was a model, but not one that was a list of them.It now decides from the declared type instead, and converts elements one at a time.
This changes the stored format
tuple[Model, ...]columns written before this will not compare equal to ones written after —union/distinct,subtractandmergebetween an old and a new dataset will disagree. Reads are unaffected: both spellings still load as the declared type, verified by writing onmainand reading here.That break is deliberate. The current spelling is a bug, the shape is rare —
tuple[Model, ...]as a stored signal appears once in this repository and nowhere insrc/orexamples/— and leaving it costs a permanent gap in #1943 (below).Deciding from the annotation, not the value
Two things follow from that, both of which an earlier revision got wrong:
list[int]andtuple[int, int]pass through untouched, so embedding vectors cost nothing to write and keep their identity for content hashing.tuple[Item, int]would otherwise callmodel_dump()on theint.Not fixed here
tuple[int, Model]is still written as an array of strings, byte-identical tomain. The warehouse chooses the array conversion from the first element, so a leading non-model sends the whole array down the per-element path. Independent of this change.Side effect
This closes the two shapes #1943 carries a strict
xfailfor. Adictarriving inside a live model has already been throughmodel_dump, which merges keys that serialize to the same JSON name; the samedictinside a list arrives intact. Flattening them alike lets the existing check see both keys, with no new code there.