Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions icechunk-python/docs/docs/guides/moving-nodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,15 @@ except ic.IcechunkError as e:
print(f"IcechunkError: {e}")
```

## Constraints

Icechunk's `move` is a rename primitive — it **never synthesizes groups**. That means:

1. **The destination's parent must already exist.** `move("/a", "/x/y/z")` fails if `/x` or `/x/y` don't exist. Create them first in a regular `writable_session`, then do the move in a `rearrange_session`.
2. **A node cannot be moved into itself or any of its own descendants.** `move("/a", "/a/c")` is rejected — such a move is not representable (the node would need to be both an ancestor and a descendant of itself). To nest a group's contents under a new descendant, create the new group yourself and move each child into it explicitly.
3. **Moves do not overwrite.** If a node already exists at the destination, the move is rejected — `move` will not silently replace it. Delete or rename the existing node first if you want it gone.
4. **The destination's parent must be a group.** `move("/a", "/arr/x")` fails when `/arr` is an array, since arrays cannot have children.

## Async API

```python
Expand Down
35 changes: 26 additions & 9 deletions icechunk-python/python/icechunk/testing/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import itertools
from collections.abc import Iterator
from dataclasses import dataclass, field
from pathlib import PurePosixPath
from typing import Any

import numpy as np
Expand Down Expand Up @@ -70,8 +71,15 @@ async def shift_array(
async def move(self, source: str, dest: str) -> None:
"""Move all keys from source to dest.

Paths must be absolute (leading ``/``); root is ``"/"``.
Store keys always have form "node/zarr.json" or "node/c/...", never bare "node".
"""
if not source.startswith("/") or not dest.startswith("/"):
raise ValueError(
f"paths must start with '/'; got source={source!r}, dest={dest!r}"
)
source = source[1:]
dest = dest[1:]
all_keys = [k async for k in self.list_prefix("")]
keys_to_move = [k for k in all_keys if k.startswith(source + "/")]
for old_key in keys_to_move:
Expand Down Expand Up @@ -107,27 +115,29 @@ class ArrayNode:
class GroupNode:
children: dict[str, ArrayNode | GroupNode] = field(default_factory=dict)

def walk(self, prefix: str = "") -> Iterator[tuple[str, Node]]:
def walk(
self, prefix: str | PurePosixPath = ""
) -> Iterator[tuple[PurePosixPath, Node]]:
"""Yield ``(path, child)`` for every node, depth-first."""
for name, child in self.children.items():
p = f"{prefix}/{name}" if prefix else name
p = PurePosixPath(prefix) / name
yield p, child
if isinstance(child, GroupNode):
yield from child.walk(p)

def nodes(self, prefix: str = "", *, include_root: bool = False) -> list[str]:
"""Return paths of all nodes, optionally including root."""
root = [prefix] if include_root else []
return root + [p for p, _ in self.walk(prefix)]
return root + [str(p) for p, _ in self.walk(prefix)]

def groups(self, prefix: str = "", *, include_root: bool = False) -> list[str]:
"""Return paths of all group nodes, optionally including root."""
root = [prefix] if include_root else []
return root + [p for p, c in self.walk(prefix) if isinstance(c, GroupNode)]
return root + [str(p) for p, c in self.walk(prefix) if isinstance(c, GroupNode)]

def arrays(self, prefix: str = "") -> list[str]:
"""Return paths of all array nodes."""
return [p for p, c in self.walk(prefix) if isinstance(c, ArrayNode)]
return [str(p) for p, c in self.walk(prefix) if isinstance(c, ArrayNode)]

def materialize(self, store: zarr.abc.store.Store) -> zarr.Group:
"""Write this tree into *store* and return the root group."""
Expand Down Expand Up @@ -158,19 +168,26 @@ def from_dict(cls, d: dict[str, Any]) -> GroupNode:
def from_paths(cls, arrays: set[str], groups: set[str]) -> GroupNode:
"""Build a GroupNode from flat sets of array and group paths.

Paths must be absolute (leading ``/``); root is ``"/"``.

Example::

GroupNode.from_paths(
arrays={"a/x", "b"},
groups={"a"},
arrays={"/a/x", "/b"},
groups={"/a"},
)
"""
for path in arrays | groups:
if not path.startswith("/"):
raise ValueError(f"path must start with '/'; got {path!r}")
tree: dict[str, Any] = {}
for path in sorted(groups - {""}):
normalized_groups = {g[1:] for g in groups}
normalized_arrays = {a[1:] for a in arrays}
for path in sorted(normalized_groups - {""}):
current = tree
for part in path.split("/"):
current = current.setdefault(part, {})
for path in sorted(arrays):
for path in sorted(normalized_arrays):
parts = path.split("/")
current = tree
for part in parts[:-1]:
Expand Down
Loading
Loading