Skip to content

[codex] Upgrade ShardedZarrStore for pyramid zarrs - #86

Merged
Faolain merged 11 commits into
mainfrom
codex/upgrade-sharded-zarr-pyramids
Jul 7, 2026
Merged

[codex] Upgrade ShardedZarrStore for pyramid zarrs#86
Faolain merged 11 commits into
mainfrom
codex/upgrade-sharded-zarr-pyramids

Conversation

@0xSwego

@0xSwego 0xSwego commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements FPAR pyramid Ticket 1 by upgrading ShardedZarrStore to support path-aware sharded_zarr_v2 manifests with one shard index per Zarr array path.

Changes

  • Added v2 per-array ArrayIndex/ChunkKey handling and path-aware chunk parsing, get/set/exists/delete/list/resize/graft behavior.
  • Preserved v1 compatibility, including legacy root c/... metadata chunks and top-level c group paths.
  • Added optional migrate_v1_to_v2(primary_array_path) support that reuses existing v1 shard blocks and rewrites metadata under the requested primary path.
  • Updated HAMT-to-sharded conversion to create v2 stores and copy metadata before chunk pointers.
  • Added v2 regression coverage for grouped pyramid arrays, coordinate arrays, migration, c-named paths, chunk-prefix listing, grafting, and malformed shard loads.

Validation

  • uv run pytest tests/test_sharded_zarr_store_v2.py tests/test_converter.py -q: 23 passed
  • uv run pre-commit run --all-files --show-diff-on-failure: passed
  • Previous full local bash run-checks.sh: 157 passed, 2 skipped, 100% coverage, hooks passed
  • Remote GitHub Actions on this head commit passed before recreating the PR

Notes

FPAR_PYRAMID_TICKETS.md was intentionally not included in this branch.

Summary by CodeRabbit

  • New Features
    • Added sharded Zarr v2 support with path-aware chunk keys and per-array indexing for grouped/nested arrays.
    • Added v1→v2 migration, creating a v2 layout while reusing existing shard content.
    • Improved directory/prefix listing for v2 stores (including zarr.json), plus more targeted grafting/resizing by array path.
  • Bug Fixes
    • Improved HAMT-to-sharded conversion to better distinguish chunk pointers vs metadata.
  • Tests
    • Expanded v2 async test coverage for byte-range reads, listing semantics, resize/delete behavior, and migration.

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The PR adds v2 per-array shard indexing to ShardedZarrStore, updates HAMT conversion to a two-pass migration into v2, and expands tests for path-aware reads, writes, resizing, migration, and validation.

Changes

ShardedZarrStore v2 and HAMT Converter

Layer / File(s) Summary
ChunkKey and ArrayIndex data models
py_hamt/sharded_zarr_store.py
Adds chunk parsing, per-array geometry modeling, versioned shard-cache keying, and cache support for v1/v2 shard addressing.
v2 root loading and shard cache
py_hamt/sharded_zarr_store.py
Adds v2 root initialization/loading, array-index registration, metadata extraction helpers, chunk-key parsing, and cache-load synchronization.
get(), set(), delete(), list(), graft_store(), resize_store(), resize_variable(), and list_dir()
py_hamt/sharded_zarr_store.py
Routes reads through parsed chunk keys, loads v2 chunks from per-array shards, centralizes pointer writes, updates existence checks, persists flushes, and extends delete, listing, graft, resize, and migration behavior for v2 paths.
HAMT converter two-phase migration to v2
py_hamt/hamt_to_sharded_converter.py
Accepts a generic content-addressed store, classifies metadata versus chunk keys, and migrates HAMT pointers into a v2 sharded store in separate metadata and chunk passes.
v2 behavior tests
tests/test_sharded_zarr_store_v2.py
Covers grouped array path awareness, resizing, concurrent mutators, in-flight writes and flushes, pointer updates, migration reuse, chunk-prefix walking, and end-to-end converter discovery.
v2 validation and key-normalization tests
tests/test_sharded_zarr_store_v2.py
Covers converter key classification, classic chunk translation, property-based mapping invariants, ArrayIndex validation, open/root validation, and invalid root and shard-cid cases.
Existing test adjustments for v2 behavior
tests/test_sharded_store_grafting.py, tests/test_sharded_zarr_store.py
Relaxes the graft out-of-bounds assertion and updates prefix listing expectations to match the new behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • dClimate/py-hamt#44: This PR updates the same converter interface to accept ContentAddressedStore, which aligns with the interface change in this diff.
  • dClimate/py-hamt#68: This PR introduces the ShardedZarrStore and converter paths that are extended here for v2 manifest handling and path-aware chunk keys.

Suggested reviewers: TheGreatAlgo, Faolain

Poem

I hopped through shards with twitching nose,
Then followed keys where data flows.
First metadata, then chunks align,
A v2 burrow, neat and fine.
🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: upgrading ShardedZarrStore for pyramid Zarr support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/upgrade-sharded-zarr-pyramids

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@py_hamt/hamt_to_sharded_converter.py`:
- Around line 12-15: Handle classic Zarr v2 chunk keys in _is_zarr_chunk_key and
the conversion flow that reaches set_pointer(), since only c/... keys are
currently recognized. Update the logic in hamt_to_sharded_converter.py to either
reject .zarray-based inputs early or translate v2 chunk keys like 0.0 and
var/0.0 into the sharded chunk-key form before pointer assignment, so they are
not treated as root metadata.

In `@py_hamt/sharded_zarr_store.py`:
- Around line 873-881: Validate the decoded shard length before storing it in
the shard cache: in sharded_zarr_store.py, after dag_cbor.decode in the shard
decode path, add a length check on decoded_shard against
array_index.chunks_per_shard before building shard_data and calling
_shard_data_cache.put. Keep the existing CID/type validation in place, but
reject malformed shards with the wrong number of entries early so later indexed
reads do not fail.
- Around line 150-157: Resizing in sharded_zarr_store.py currently only
truncates shard CID slots in the resize flow, which leaves stale chunk data in
the retained tail shard when the array shrinks. Update the resize logic around
the shard CID handling to also clear any chunk entries whose offsets are beyond
total_chunks in the final retained shard, and make sure any cached shard/chunk
entries are evicted or refreshed so old data cannot reappear after a later grow.
Apply the same cleanup in the related resize paths tied to the shard CID updates
and cache maintenance.
- Around line 1158-1196: The flush path in ShardedZarrStore currently snapshots
dirty shard data and calls mark_clean() without holding the corresponding
_shard_locks, so a concurrent set() or delete() can mutate the shard while
cas.save() is in progress and then get incorrectly cleared. Update the flush
logic around the dirty_shards loop to acquire the shard lock for each cache_key
before reading from _shard_data_cache, saving via cas.save, and marking the
shard clean, and keep the lock held until the cache entry is safely updated; use
the existing _shard_locks, _shard_data_cache, and mark_clean flow as the place
to apply the fix.
- Around line 1315-1316: The public set_pointer mutator currently bypasses the
same read-only write guard used by set(), delete(), resize_store(), and
graft_store(). Update set_pointer in ShardedZarrStore to enforce the read-only
check before calling _set_pointer, using the same guard pattern already applied
in the other mutating methods so pointer updates are blocked when the store is
read-only.
- Around line 1058-1062: The timeout handling in sharded_zarr_store.py should
explicitly chain the caught asyncio.TimeoutError when re-raising the
RuntimeError in this load path. Update the except asyncio.TimeoutError block in
the shard loading logic to preserve the original exception context while keeping
the existing pending-load cleanup for _pending_shard_loads and the timeout
message that references shard_idx.
- Around line 683-688: Reject chunk-shape updates for an existing array index in
sharded_zarr_store.py: when the existing index is reused in the branch that
updates existing.array_shape and existing.chunk_shape, validate that the
incoming chunk_shape matches the stored one and fail instead of overwriting it.
Keep the fix localized to the existing array index handling path in the array
index creation/update logic so coordinate-to-slot mapping cannot change for
already-written shards.
- Around line 297-305: The _V1_COORDINATE_ARRAY_PREFIXES class attribute in
ShardedZarrStore is currently mutable and triggers Ruff; update the symbol to be
an immutable ClassVar backed by a frozenset so it cannot be modified as shared
class state. Keep the same coordinate prefixes, but change the declaration and
any related type annotation in the class definition to use ClassVar and
frozenset to satisfy linting.
- Around line 1324-1335: Invalidate any cached entry in _metadata_read_cache
when set_pointer() updates a metadata pointer so later get() calls do not return
stale bytes. Update the metadata-pointer branch in
ShardedZarrStore.set_pointer() to remove or refresh the cache entry for that key
before returning, and make sure the fix covers keys handled through
_array_path_from_metadata_key() and the existing
_register_array_metadata_from_bytes() flow.

In `@tests/test_sharded_zarr_store_v2.py`:
- Around line 69-906: The v2 path-aware chunk/index behavior in ShardedZarrStore
is only covered by example-based tests, so add at least one Hypothesis invariant
test in test_v2_sharded_zarr_store_v2.py. Use the existing helpers and symbols
such as ShardedZarrStore.open, list_dir, list_prefix, get, exists, and
ArrayIndex to generate varied array/group paths and chunk layouts, then assert
the v2 invariants still hold across those combinations. Keep the test focused on
the new v2 routing/metadata behavior rather than duplicating the current fixed
cases.
- Around line 41-47: The helper method signature in load uses the built-in name
id, which will trigger Ruff A002 under the tests lint rules. Rename that
parameter to a non-builtin identifier in the load method of the test helper
class, and update any internal references to match so the test file continues to
pass ruff check.

In `@tests/test_sharded_zarr_store.py`:
- Around line 785-786: The assertion in the directory listing check is too weak
because it only verifies zarr.json, which would miss regressions where chunk
entries are no longer returned. Update the check around prefixed_dir_keys in the
test to also assert that the expected chunk directory entry c is present, so
list_dir(prefix) is validated for both metadata and chunk contents.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e52f2759-26cf-43ea-9d50-adb7b40e41b0

📥 Commits

Reviewing files that changed from the base of the PR and between e932534 and ac1d1d2.

📒 Files selected for processing (5)
  • py_hamt/hamt_to_sharded_converter.py
  • py_hamt/sharded_zarr_store.py
  • tests/test_sharded_store_grafting.py
  • tests/test_sharded_zarr_store.py
  • tests/test_sharded_zarr_store_v2.py

Comment thread py_hamt/hamt_to_sharded_converter.py Outdated
Comment thread py_hamt/sharded_zarr_store.py
Comment thread py_hamt/sharded_zarr_store.py Outdated
Comment thread py_hamt/sharded_zarr_store.py
Comment thread py_hamt/sharded_zarr_store.py
Comment thread py_hamt/sharded_zarr_store.py Outdated
Comment thread py_hamt/sharded_zarr_store.py
Comment thread tests/test_sharded_zarr_store_v2.py
Comment thread tests/test_sharded_zarr_store_v2.py
Comment thread tests/test_sharded_zarr_store.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_sharded_zarr_store_v2.py`:
- Line 310: The test helpers using monkeypatch are missing an explicit type
annotation, which violates the fully typed test rule. Update the affected test
functions in test_sharded_zarr_store_v2, including
test_v2_resize_blocks_interleaved_chunk_mutators and the other monkeypatch-based
tests in that file, so the monkeypatch fixture parameter is annotated as
pytest.MonkeyPatch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f05c5aaa-d3dd-4afd-b77d-30350f61861f

📥 Commits

Reviewing files that changed from the base of the PR and between ac1d1d2 and 757de4c.

📒 Files selected for processing (3)
  • py_hamt/hamt_to_sharded_converter.py
  • py_hamt/sharded_zarr_store.py
  • tests/test_sharded_zarr_store_v2.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • py_hamt/hamt_to_sharded_converter.py
  • py_hamt/sharded_zarr_store.py

Comment thread tests/test_sharded_zarr_store_v2.py Outdated
@codecov-commenter

codecov-commenter commented Jun 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (e932534) to head (13f1811).

Additional details and impacted files
@@            Coverage Diff             @@
##              main       #86    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files            8         8            
  Lines         1617      2541   +924     
==========================================
+ Hits          1617      2541   +924     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

if not isinstance(cid, CID): # pragma: no cover
raise TypeError(f"Expected CID pointer for key {key!r}.")
cid_base32_str = str(cid.encode("base32"))
await dest_store.set_pointer(chunk_key, cid_base32_str)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding: converted Zarr v2 chunks are written under v3-style .../c/... keys, but the .zarray / .zgroup metadata is copied unchanged. Zarr v2 readers still request keys like var/0 or 0.0, so converted stores can read back fill values instead of the original chunk data.

Suggested solution: either preserve the original v2 chunk keys when the source metadata is v2, or fully migrate both metadata and consolidated metadata to a valid v3 layout. Please add an xr.open_zarr round-trip test for a zarr_format=2 HAMT source.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled in 564f0a2. Converted Zarr v2 HAMT sources now keep v2 group metadata (.zgroup) instead of injecting v3 zarr.json, and ShardedZarrStore resolves classic v2 chunk requests such as var/0, var/0.1, and root numeric chunk keys through the path-local shard index. Added an xr.open_zarr round-trip test for a zarr_format=2 HAMT source.

Validation: uv run pytest tests/test_sharded_zarr_store_v2.py, uv run pre-commit run --all-files --show-diff-on-failure, and uv run pytest --cov=py_hamt tests/ --ignore=tests/test_public_gateway.py.

if self._manifest_version != SHARDED_ZARR_V2:
return keys

for array_path in self.array_indices:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding: v2 directory listing derives entries from array_indices, but delete/clear paths only remove metadata and chunk pointers. After delete_dir("a"), stale array_indices["a"] keeps a visible in listings and can block overwrites with new geometry.

Suggested solution: override delete_dir / clear for v2, or prune matching array_indices, shard cache keys, pending loads, and root arrays entries when deleting an array subtree. Please add tests for delete_dir(""), delete_dir("a"), and to_zarr(mode="w") overwrite with changed shape or chunk shape.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled in 564f0a2. v2 delete_dir and clear now prune matching array_indices, root arrays entries, shard cache entries, and pending shard-load events. delete_dir("a"), delete_dir(""), clear(), and to_zarr(mode="w") overwrites with changed geometry are covered.

Validation: uv run pytest tests/test_sharded_zarr_store_v2.py, uv run pre-commit run --all-files --show-diff-on-failure, and uv run pytest --cov=py_hamt tests/ --ignore=tests/test_public_gateway.py.

metadata_key = (
"zarr.json" if group_path == "" else f"{group_path}/zarr.json"
)
metadata[metadata_key] = await self.cas.save(group_metadata, codec="raw")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding: this writes default parent group metadata unconditionally. During v1-to-v2 migration, any existing root or group zarr.json metadata is replaced with an empty group document, so group attributes are lost.

Suggested solution: only insert the default group metadata when the metadata key is absent; preserve existing CIDs/data otherwise. Please add a migration regression test with root/group attributes to prove they survive.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled in 564f0a2. Migration now preserves existing group metadata CIDs instead of overwriting them with empty defaults, and strips stale consolidated metadata from preserved v3 group docs so relocated groups can still be discovered. Added a migration regression test for preserved root and group attributes.

Validation: uv run pytest tests/test_sharded_zarr_store_v2.py, uv run pre-commit run --all-files --show-diff-on-failure, and uv run pytest --cov=py_hamt tests/ --ignore=tests/test_public_gateway.py.

)
self._dirty_root = True

async def migrate_v1_to_v2(self, primary_array_path: str) -> str:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding: migrate_v1_to_v2() mutates the store and flushes a new root even when the instance was opened read-only. This differs from set, delete, grafting, and resize APIs, which reject writes on read-only stores.

Suggested solution: add the same PermissionError guard before migration does any mutation or flush, and add a read-only migration regression test.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled in 564f0a2. migrate_v1_to_v2() now raises PermissionError immediately on read-only stores before acquiring the write path or flushing. Added a read-only migration regression test.

Validation: uv run pytest tests/test_sharded_zarr_store_v2.py, uv run pre-commit run --all-files --show-diff-on-failure, and uv run pytest --cov=py_hamt tests/ --ignore=tests/test_public_gateway.py.

Comment thread py_hamt/sharded_zarr_store.py Outdated
if parsed_chunk is None:
if register_metadata:
await self._ensure_v2_parent_group_metadata(key)
self._root_obj["metadata"][key] = pointer_cid_obj

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding: metadata is written into _root_obj before the new metadata is validated and registered against the array index. If _register_array_metadata_from_bytes raises, for example on a chunk-shape change, the store is left with new metadata but the old ArrayIndex geometry.

Suggested solution: validate/register the metadata before mutating _root_obj, or roll back both _root_obj and caches on failure. Metadata and ArrayIndex updates should be atomic for shape/chunk-shape changes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled in 564f0a2. Metadata pointer updates now load and register array metadata before mutating _root_obj["metadata"]; failed chunk-shape validation leaves the old metadata pointer and ArrayIndex intact. Added a regression test for failed set_pointer() metadata registration.

Validation: uv run pytest tests/test_sharded_zarr_store_v2.py, uv run pre-commit run --all-files --show-diff-on-failure, and uv run pytest --cov=py_hamt tests/ --ignore=tests/test_public_gateway.py.

@Faolain

Faolain commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Codex Generated PR Doc:

What PR 86 Does

PR 86 upgrades ShardedZarrStore so it can store pyramid/grouped Zarr layouts where each array path has its own chunk grid. The core change is moving from one global shard index for the whole store to one shard index per array path.

Before this PR, ShardedZarrStore was effectively a single-array layout: one array_shape, one chunk_shape, one chunks_per_shard, and one global shard_cids list. That does not work well for pyramid Zarrs like:

0/FPAR/c/...
1/FPAR/c/...
2/FPAR/c/...
0/x/c/...
0/y/c/...

because each array can have different shape/chunk geometry, but old sharding assumed one shared geometry.

The PR adds a new manifest format:

SHARDED_ZARR_V1 = "sharded_zarr_v1"
SHARDED_ZARR_V2 = "sharded_zarr_v2"

In v2, the root object has:

metadata: {zarr metadata key -> CID}
arrays: {
  "0/FPAR": {
    array_shape,
    chunk_shape,
    sharding_config,
    shard_cids
  },
  "1/FPAR": ...
}

So each array path gets its own ArrayIndex.

Main Implementation Changes

py_hamt/sharded_zarr_store.py is the bulk of the PR.

It adds:

  • ChunkKey: parsed chunk key representation: array path plus chunk coordinates.
  • ArrayIndex: per-array shape, chunk shape, chunks-per-shard, derived chunk counts, shard count, and shard CID list.
  • v2 root initialization and loading.
  • v1/v2 manifest compatibility.
  • path-aware chunk parsing for keys like 0/FPAR/c/0/0/0.
  • tuple cache keys like ("0/FPAR", 3) so shards from different arrays do not collide.
  • metadata-driven array registration: when a/zarr.json or a/.zarray is written, the store extracts shape/chunk metadata and creates or updates array_indices["a"].

The public ShardedZarrStore.open() behavior is extended:

  • Shape-based construction remains the v1 compatibility path.
  • Passing manifest_version=SHARDED_ZARR_V2, or creating without array_shape / chunk_shape, creates a v2 store.
  • Existing roots load as v1 or v2 based on manifest_version.

Behavior Changes

Reads/writes/deletes now route chunk keys through the relevant array index:

0/FPAR/c/0/0/0 -> array "0/FPAR", coords (0, 0, 0)
1/FPAR/c/0/0/0 -> array "1/FPAR", coords (0, 0, 0)

Those two keys can now point to different shard lists and different geometries.

Listing is also expanded. For v2, list() yields metadata and generated chunk keys, while list_dir() can walk nested groups and explicit chunk prefixes like:

list_dir("")
list_dir("0")
list_dir("0/FPAR")
list_dir("0/FPAR/c")

Resize is made array-local. resize_store(..., array_path="0/FPAR") resizes only that array’s index. The resize logic snapshots old shard contents, remaps retained chunks by coordinates, drops chunks now out of bounds, and replaces affected shards. There are also locks/events to prevent interleaved writes during resize.

Grafting is updated so source and target arrays can be selected by path:

graft_store(..., source_array_path="0/FPAR", target_array_path="1/FPAR")

Migration And Conversion

The PR adds migrate_v1_to_v2(primary_array_path). It rewrites a v1 root into a v2 root, reusing the old shard CID list under the requested array path. It also rewrites metadata keys so a previous single-array store can be mounted under a grouped path like 0/FPAR.

py_hamt/hamt_to_sharded_converter.py is updated to create v2 sharded stores. It now copies metadata first so array indices are registered before copying chunk pointers. It also adds helpers to classify Zarr metadata vs chunk keys and normalize some classic Zarr chunk key forms into v3-style .../c/... keys.

Tests Added

The PR adds tests/test_sharded_zarr_store_v2.py with broad coverage for:

  • grouped pyramid arrays,
  • coordinate arrays,
  • per-array resize,
  • resize concurrency,
  • v1-to-v2 migration,
  • root-array c/... behavior,
  • top-level c group/path edge cases,
  • chunk-prefix listing,
  • graft bounds validation,
  • malformed root/shard validation,
  • converter key classification,
  • classic Zarr v2 chunk translation,
  • property-style chunk index mapping invariants.

It also tweaks existing sharded store tests for the new behavior.

Current PR State

Current head is ce91e386.... GitHub checks are green. The PR still has substantive review comments around conversion correctness, stale v2 indices after deletes/overwrites, migration metadata preservation, metadata/index atomicity, and read-only migration behavior.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
py_hamt/sharded_zarr_store.py (1)

1091-1130: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unhandled ValueError from _parse_chunk_key can crash get()/_delete_unlocked()/_set_pointer().

The new fallback at Line 1117-1123 re-raises the int() ValueError when a key matches the /c/-or-c/-prefix shape but has non-numeric coordinates and doesn't match any classic v2 key pattern. exists() explicitly catches (ValueError, IndexError, KeyError) around this call, but get() (Line 1493), _delete_unlocked() (Line 1677), and _set_pointer() (Line 1595) call _parse_chunk_key unguarded. A key such as "c/FPAR/not-a-chunk" — confirmed in this PR's own test (assert not await classic_store.exists("c/FPAR/not-a-chunk"), only exercised via exists()) — would raise instead of being treated as "not found" if the same key reached get().

🐛 Proposed fix
             kind = "metadata"
             shard_idx_for_trace: int | None = None
-            parsed_chunk = self._parse_chunk_key(key)
+            try:
+                parsed_chunk = self._parse_chunk_key(key)
+            except (ValueError, IndexError):
+                return None
             try:

Apply the same guard at the start of _delete_unlocked and _set_pointer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@py_hamt/sharded_zarr_store.py` around lines 1091 - 1130, _handle invalid
chunk-key parsing in the same way across all callers of _parse_chunk_key. The
new ValueError path for malformed /c/ or c/ keys is only tolerated by exists(),
but get(), _delete_unlocked(), and _set_pointer() still call _parse_chunk_key
directly and can crash on non-numeric coordinates. Add the same defensive
handling used by exists() around _parse_chunk_key in _delete_unlocked and
_set_pointer, and make sure get() also treats these malformed keys as missing
rather than propagating ValueError.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@py_hamt/sharded_zarr_store.py`:
- Around line 1711-1730: `delete_dir` is doing an expensive full-store chunk
enumeration by calling `list_prefix(match_prefix)`, which ultimately scans every
v2 shard across all arrays instead of only the target subtree. Update
`delete_dir` to enumerate only `_root_obj["metadata"]` for individual
`_delete_unlocked` cleanup, and remove the chunk-key loop for shard contents
under the prefix. Keep the existing
`_prune_v2_array_indices_for_prefix(normalized_prefix)` call to handle shard
cache and array-index cleanup efficiently, and preserve the read-only and
root-prefix behavior in `delete_dir`.

---

Outside diff comments:
In `@py_hamt/sharded_zarr_store.py`:
- Around line 1091-1130: _handle invalid chunk-key parsing in the same way
across all callers of _parse_chunk_key. The new ValueError path for malformed
/c/ or c/ keys is only tolerated by exists(), but get(), _delete_unlocked(), and
_set_pointer() still call _parse_chunk_key directly and can crash on non-numeric
coordinates. Add the same defensive handling used by exists() around
_parse_chunk_key in _delete_unlocked and _set_pointer, and make sure get() also
treats these malformed keys as missing rather than propagating ValueError.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a6961e7f-df26-4c1a-b3d1-03d3734236e2

📥 Commits

Reviewing files that changed from the base of the PR and between ce91e38 and 564f0a2.

📒 Files selected for processing (2)
  • py_hamt/sharded_zarr_store.py
  • tests/test_sharded_zarr_store_v2.py

Comment thread py_hamt/sharded_zarr_store.py
@0xSwego

0xSwego commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

Handled CodeRabbit outside-diff malformed chunk-key feedback in 415e1f2. For v2 stores, malformed chunk-looking keys such as c/FPAR/not-a-chunk now behave as missing across get(), delete(), set_pointer(), and set() instead of propagating parser ValueError/IndexError; v1 keeps its previous strict exceptions for backward compatibility. Added regression coverage for both the new v2 behavior and preserved v1 raise behavior.

Validation: uv run pytest tests/test_sharded_zarr_store_v2.py; uv run pre-commit run --all-files --show-diff-on-failure; uv run pytest --cov=py_hamt tests/ --ignore=tests/test_public_gateway.py.

@Faolain
Faolain merged commit 8ee103c into main Jul 7, 2026
3 checks passed
@0xSwego
0xSwego deleted the codex/upgrade-sharded-zarr-pyramids branch July 7, 2026 17:50
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.

3 participants