fix: normalize models inside collections before they reach the warehouse - #1964
Open
shcheklein wants to merge 1 commit into
Open
fix: normalize models inside collections before they reach the warehouse#1964shcheklein wants to merge 1 commit into
shcheklein wants to merge 1 commit into
Conversation
Deploying datachain with
|
| Latest commit: |
0dc3227
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://7758a407.datachain-2g6.pages.dev |
| Branch Preview URL: | https://fix-flatten-model-collection.datachain-2g6.pages.dev |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
A model inside a list is replaced by its dump before storage; the same model
inside a tuple, a mapping value, or a UDF's returned collection is not. It
arrives at the warehouse live and is converted there instead, by different
rules, so the same data is written two ways:
list[Item] [{"n":1}]
tuple[Item, ...] ["{\"n\":1}"]
_flatten_fields_values decided what to convert from the runtime type of a
value and knew only list and dict; its dict branch converted a value that was
itself a model but not one that was a list of them. flatten_value, which UDF
outputs go through, converted nothing at all unless the whole annotation was a
model.
Both now share one normalizer that walks by declared type. A type that cannot
hold a model is returned as it is, so a vector of numbers is not copied, and
this holds per element: tuple[Item, list[int]] converts the model and leaves the
list alone. Optional is unwrapped before arguments are read, so list[Item] | None
and None | list[Item] behave the same. Elements are converted individually rather
than from whatever the first one is, which a fixed-length tuple needs. An erased
annotation -- Any, object, a bare container -- says nothing about its contents
and is walked rather than assumed model-free.
Models are dumped in Pydantic's python mode, which is what the list path already
used: it leaves values as Python objects for the encoder, which knows numpy and
the datetime types. That makes a numpy field work inside a tuple, where it
failed before, and stops a serializer declared for JSON output from replacing a
stored value with its presentation form -- a tuple of models with a redacting
serializer used to persist the redaction and lose the original. A path field
inside a tuple no longer serializes, matching every other shape.
tuple[Model, ...] columns written before this do not compare equal to ones
written after. Reads are unaffected: both spellings load as the declared type.
shcheklein
force-pushed
the
fix/flatten-model-collections
branch
from
August 30, 2026 00:15
f5d830d to
0dc3227
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The same model is stored two different ways depending on the container it sits in.
What lands in the column:
Why
A model inside a
listis replaced by its dump before it reaches storage. Inside a tuple, a mapping value, or a UDF's returned collection it is not — it arrives live and the warehouse converts it there instead, by different rules.Two places decide this and neither did it completely:
_flatten_fields_valueschose from the runtime type of the value and knew onlylistanddict. Itsdictbranch converted a value that was a model, but not one that was a list of them.flatten_value, which UDF outputs go through, converted nothing unless the whole annotation was a model — so a UDF returninglist[Item]handed over live models.Both now share one normalizer that walks by declared type.
What that buys, beyond consistency
tuple[Model, ...]when_used="json"serializer'***', original lostDecimal"1.20"1.2, same as a listPathfieldThe serializer row is the one worth reading twice: a serializer written to hide a value in an API response was deciding what went into the database, and the original was unrecoverable.
Deciding from the annotation
list[int]is never copied — and this holds per element, sotuple[Item, list[int]]converts the model and leaves the list alone.Optionalis unwrapped before arguments are read, solist[Item] | NoneandNone | list[Item]behave identically.tuple[Item, int]needs.Any,object, a bare container — says nothing about its contents, so it is walked rather than assumed model-free.Compatibility
tuple[Model, ...]columns written before this will not compare equal to ones written after —filter,distinct,subtract,merge. Reads are unaffected: verified by writing such a column onmainand reading it here, where both spellings load as the declared tuple.Relationship to #1943
Independent — neither needs the other. But together they close two of that PR's strict
xfails: a colliding mapping inside a live model reachesmodel_dump(mode="json"), which merges the keys before anything can inspect them. Normalizing first means it arrives as a plain dict and the existing check sees both keys. UDF-returnedlist[Model]anddict[str, Model]are covered too. Whichever lands second should flip those markers.