Skip to content

fix(chunk-grids): one invariant for zero-length axes across model, clamps, and metadata - #4334

Draft
d-v-b wants to merge 6 commits into
zarr-developers:mainfrom
d-v-b:fix/zero-length-single-invariant
Draft

fix(chunk-grids): one invariant for zero-length axes across model, clamps, and metadata#4334
d-v-b wants to merge 6 commits into
zarr-developers:mainfrom
d-v-b:fix/zero-length-single-invariant

Conversation

@d-v-b

@d-v-b d-v-b commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

🤖 AI text below 🤖

Makes chunk sizes positive while allowing zero-length array axes. FixedDimension now rejects size=0, and full-span chunk inference uses a shared helper returning max(span, 1). Rectilinear creation accepts a non-empty list of positive chunk sizes on an empty axis and retains those sizes for later growth.

For legacy V2 metadata, a stored chunk edge of 0 on an axis with extent 0 is interpreted as chunk size 1 with a ZarrUserWarning. A zero chunk edge on a positive-length axis is rejected. This is a compatibility policy for the current grid model; the metadata does not prove whether chunk payloads exist in the store.

The historical evidence is narrower than the original description claimed. In an isolated Python 3.12 environment with zarr 2.18.7, NumPy 2.2.6 and numcodecs 0.15.1, creating an empty int32 array with chunks=False, -1, or (0,) stores chunks:[0]. For each spelling, nchunks, full reads, empty full writes, append, resize, and reopen-then-read raise ZeroDivisionError. Attribute updates and attribute reads succeed, so saying these arrays could not perform any operation was incorrect. This test does not establish behavior for every 2.x release or every release from 3.0 through 3.3, and those universal claims have been removed.

Tests cover six chunk spellings over empty/scalar shapes and applicable format/sharding combinations: 124 cases execute and 26 unsupported combinations are skipped. Separate cases check invalid chunk edges and positive-extent V2 rejection. Audit validation: hatch run test.py3.12-minimal:pytest tests/test_metadata/test_v2.py tests/test_chunk_grids.py tests/test_unified_chunk_grid.py -q — 681 passed, 26 skipped. The former 7409-test total was a historical run, not reproduced in this audit.

The legacy-reader choice remains reviewable: normalize empty-axis zero edges with a warning, as implemented here, or reject them. Neither choice requires claiming that no historical reader ever handled such metadata. This PR does not reconstruct a universal history of zero-length-axis bugs or attribute that history to a counted set of authors.

@d-v-b
d-v-b force-pushed the fix/zero-length-single-invariant branch from 7c5688b to 302b638 Compare September 9, 2026 18:28
…, clamps and metadata

Invariant: a chunk edge length is always >= 1; a dimension's extent may be 0,
in which case the dimension has zero chunks (ceildiv(0, size) == 0).

Zero-length-axis bugs have recurred since 2017 (#150, #241, #303, zarr-developers#972,
zarr-developers#1977, zarr-developers#2434, zarr-developers#3711, zarr-developers#4305, zarr-developers#4307, zarr-developers#4328) because the layers disagreed on
this invariant and every span-derived chunk spelling clamped on its own:

- The metadata layer (common.py, metadata/v3.py) required chunk edges >= 1,
  but the in-memory FixedDimension allowed size == 0 with four special-case
  branches left over from zarr-developers#2434, so normalization could build a grid the
  metadata constructor then rejected. FixedDimension now rejects size < 1
  and the four `if self.size == 0` branches are gone. VaryingDimension
  already required edges > 0 and is unchanged.
- `chunks=-1`, `chunks=False`, `chunks="auto"` (_guess_regular_chunks, both
  the typesize == 0 early return and the np.maximum line) and `shards="auto"`
  each derived "one chunk covering the axis" independently. They now all go
  through one helper, `_full_span_chunk_size(span) = max(span, 1)`, which is
  the single definition of that phrase for a possibly zero-length axis.
- Zarr format 2 metadata had no chunk >= 1 check, so a legacy `chunks: [0]`
  document opened fine and read uninitialised memory after a resize. It now
  raises a clear ValueError at parse time, matching the format 3 grid.
- Rectilinear grids had no creation-time spelling for a zero-length axis:
  normalize_chunks_1d required sum(edges) == span, which no list of positive
  edges can satisfy for span 0, even though the same state is reachable via
  resize((0,)) and round-trips through reopen. For span == 0 any non-empty
  list of positive edges is now accepted verbatim, producing the same
  VaryingDimension(edges, extent=0) that resize produces; the strict sum
  check is kept for span > 0.

Tests: the per-spelling regression test from zarr-developers#4328 is replaced by one matrix
over {-1, False, "auto", 1, (1,...), [[2, 2]]} x {(0,), (0, 4), (4, 0),
(0, 0), ()} x {v2, v3} x {no shards, shards="auto" with and without a byte
budget, explicit shards}, with separate small tests for each error case.
Tests that constructed FixedDimension(size=0) now assert it raises, and a
zero-extent test covers the behaviour the old special cases were guarding.

Assisted-by: ClaudeCode:claude-fable-5-1
zarr-python 2.18.7 writes `chunks: [0]` for `zarr.zeros((0,), chunks=False)`
and for `chunks=(0,)`, so stores with that document exist. Rejecting them
at open would turn a previously-readable array into an error; leaving the
0 in place read uninitialised memory after a resize. Normalize the edge to
1 with a ZarrUserWarning instead — the same grid every other "one chunk
spans the axis" spelling produces — and keep rejecting a zero edge on an
axis that has data.

Assisted-by: ClaudeCode:claude-fable-5-1
Assisted-by: ClaudeCode:claude-fable-5-1
Measured against zarr 2.18.7: `zeros((0,), chunks=False)`, `chunks=-1` and
`chunks=(0,)` all write `chunks: [0]`, after which nchunks, read, write,
append, resize and reopen-then-read every raise ZeroDivisionError. There
was never a working behaviour to preserve; normalizing the edge to 1 makes
such arrays usable for the first time. Say so in the comment and fragment
instead of claiming the stores were previously readable.

Assisted-by: ClaudeCode:claude-fable-5-1
@d-v-b
d-v-b force-pushed the fix/zero-length-single-invariant branch from 302b638 to 047a92e Compare September 9, 2026 18:28
@codecov

codecov Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 94.34%. Comparing base (ba883a5) to head (2297c62).

Files with missing lines Patch % Lines
src/zarr/core/chunk_grids.py 90.90% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4334   +/-   ##
=======================================
  Coverage   94.34%   94.34%           
=======================================
  Files          92       92           
  Lines       12935    12938    +3     
=======================================
+ Hits        12203    12206    +3     
  Misses        732      732           
Files with missing lines Coverage Δ
src/zarr/core/metadata/v2.py 89.89% <100.00%> (+0.50%) ⬆️
src/zarr/core/chunk_grids.py 96.73% <90.90%> (-0.06%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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