Repository navigation
Conversation
The cast_value spec requires the fill value to survive a forward and inverse cast with the codec's configuration, and requires an error when it does not. resolve_metadata cast the fill value forward only, so out_of_range "clamp" or "wrap", or rounding, accepted a fill value that decodes to something else. A partly written chunk then read that value where it should read the fill value: float32 fill 1000.0 with an int8 clamp target read back as 127.0. resolve_metadata, which ArrayV3Metadata runs at construction, now casts the encoded fill back with the decode scalar_map and compares it with the original. NaN matches NaN; the sign of zero must match. A fill value whose forward or inverse cast fails gets an error that names it as the fill value. Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4510 +/- ##
==========================================
+ Coverage 94.71% 94.72% +0.01%
==========================================
Files 94 94
Lines 13663 13700 +37
==========================================
+ Hits 12941 12978 +37
Misses 722 722
🚀 New features to boost your workflow:
|
Move the round-trip check out of `CastValue.resolve_metadata`, which runs for every chunk and which `ShardingCodec.validate` probes with the data type's default scalar, into `check_fill_value_round_trip`, which threads the array's fill value through the codecs, descending into shards. Array creation calls it and raises; `ArrayV3Metadata.from_dict` calls it and warns, so an array already stored with such a fill value still opens and reads as before. Assisted-by: ClaudeCode:claude-fable-5-1
`resolve_metadata` and `check_fill_value` each cast a one-element fill array and wrapped the backend's error with the fill value and both data types. `_cast_fill` does that in either direction for both of them. Assisted-by: ClaudeCode:claude-fable-5-1
Assisted-by: ClaudeCode:claude-fable-5-1
…array opens Assisted-by: ClaudeCode:claude-fable-5-1
`_check_fill_value` and `_check_fill_value_round_trip` are called only by array creation and metadata reading, so they are not public API. Assisted-by: ClaudeCode:claude-fable-5-1
This branch has not been deployed
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.
🤖 AI text below 🤖
The
cast_valuecodec now checks that the array's fill value survives a round trip through the codec, as thecast_valuespec requires. Previouslyout_of_range="clamp"or"wrap", or rounding, could encode the fill value to something that decodes to a different value, so the unwritten part of a partly written chunk read back as that value instead of the fill value. For example, afloat32array with fill value1000.0and anint8target with clamping read127.0there. Creating such an array now raises aValueErrorthat names the fill value, both data types, and the value it decodes to. Opening an array already stored with such a fill value still works: it keeps its fill value, and a warning says what its partly written chunks hold and how to fix it. The check runs once when metadata is created or read, not for every chunk, and the codecs inside a shard are checked with the array's fill value rather than the data type's default scalar, so a shard's codec chain is no longer refused for a fill value it would never be asked to encode.Spec
From the
data_typesection of thecast_valuespec:CastValue.resolve_metadatacast the fill value forward only, with the encodescalar_map, and never checked the inverse.Reproduction (before this change, cast-value-rs 0.4.2)
Element 1 sits in a partly written chunk, so it is stored as the encoded fill (127) and decodes to 127.0. Elements 2 and 3 are in a chunk that was never written, so they read the fill directly. After this change,
create_arrayraises:An array already stored that way opens with a
ZarrUserWarningthat carries the same text, followed by what it means for the stored chunks and how to fix the array, and reads exactly as before.Where the check runs
The check is not in
resolve_metadata, which the chunk pipeline calls for every chunk read or written and whichShardingCodeccalls with the data type's default scalar when it validates its inner codecs. Instead:CastValue._check_fill_value(spec)does the round trip for one codec and raises._check_fill_value_round_trip(metadata)threads the array's fill value through the codecs, as a chunk is encoded, and calls_check_fill_valueon everycast_valuecodec with the fill value it would receive, descending into the codecs of a sharding codec.AsyncArray._create_metadata_v3, which every array creation goes through, calls it and lets the error propagate.ArrayV3Metadata.from_dictcalls it after constructing the metadata and turns the error into aReadingwarning through the existing metadata repair machinery, so an existing array stays readable.resolve_metadatakeeps casting the fill value forward only, as it did before, so the per-chunk cost is unchanged. It now wraps a failed forward cast in aValueErrorthat names the fill value and both data types, which_check_fill_valuereuses.Tests
In
tests/test_codecs/test_cast_value.py:test_fill_value_roundtrip_accepted[...](7 cases)test_fill_value_partial_chunk_without_check_reads_wrong_valuetest_fill_value_checked_inside_shardsScaleOffset(offset=100)then uint16 clamp, fill 100.0; shards with int8 clamp, fill 1000.0test_resolve_metadata_only_casts_forwardresolve_metadatareturns 127 without raisingtest_fill_value_clamp_out_of_range_rejectedtest_fill_value_wrap_out_of_range_rejectedtest_fill_value_lost_to_rounding_rejectedtest_fill_value_signed_zero_lost_rejectedtest_fill_value_nan_to_int_without_scalar_map_rejectedtest_fill_value_cannot_decode_rejectedResults with
uv sync --extra cast-value-rs --extra optional --group test(cast-value-rs 0.4.2):tests/(full suite,-n auto, withouttests/test_regression): 12520 passed, 1276 skipped, 4 xfailedprek runon the changed files: ruff check, ruff format, codespell, numpydoc validation and the rest pass; the mypy hook reports only the pre-existingtype: ignore[no-any-return]in_do_castas unused, because this environment has thecast-value-rsextra installed and the package ships type stubs, which the lockfile environment CI uses does not haveBookkeeping
mainat bd0dc84.resolve_metadata. That refused to open arrays already stored with such a fill value, doubled the per-chunk casts, and rejected shardedScaleOffsetplus clamping chains through the default-scalar probe ofShardingCodec.validate(the sharded half of ShardingCodec.validate incorrectly assumes that Zdtype.default_scalar is always a valid value #4511).ShardingCodec.validateprobes its inner codecs, so an inner chain that cannot encode the default scalar at all (no clamping or wrapping) still fails there.🤖 Generated with Claude Code