Skip to content

Commit ab8b495

Browse files
committed
Recognize .b2b extension as BatchStore in DictStore
1 parent f51c430 commit ab8b495

3 files changed

Lines changed: 49 additions & 7 deletions

File tree

src/blosc2/dict_store.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ class DictStore:
3636
are stored as .b2nd files.
3737
- blosc2.SChunk: super-chunks. When persisted externally they are stored
3838
as .b2f files.
39+
- blosc2.BatchStore: batched variable-length containers. When persisted
40+
externally they are stored as .b2b files.
3941
- blosc2.C2Array: columnar containers. These are always kept inside the
4042
embedded store (never externalized).
4143
- numpy.ndarray: converted to blosc2.NDArray on assignment.
@@ -91,7 +93,7 @@ class DictStore:
9193
Notes
9294
-----
9395
- External persistence uses the following file extensions:
94-
.b2nd for NDArray and .b2f for SChunk.
96+
.b2nd for NDArray, .b2f for SChunk, and .b2b for BatchStore.
9597
"""
9698

9799
def __init__(
@@ -181,8 +183,11 @@ def _init_read_mode(self, dparams: blosc2.DParams | None = None):
181183
dparams=dparams,
182184
)
183185
for filepath in self.offsets:
184-
if filepath.endswith((".b2nd", ".b2f")):
185-
key = "/" + filepath[: -5 if filepath.endswith(".b2nd") else -4]
186+
if filepath.endswith((".b2nd", ".b2f", ".b2b")):
187+
if filepath.endswith(".b2nd"):
188+
key = "/" + filepath[:-5]
189+
else:
190+
key = "/" + filepath[:-4]
186191
self.map_tree[key] = filepath
187192
else: # .b2d
188193
if not os.path.isdir(self.localpath):
@@ -228,14 +233,14 @@ def _update_map_tree(self):
228233
for root, _, files in os.walk(self.working_dir):
229234
for file in files:
230235
filepath = os.path.join(root, file)
231-
if filepath.endswith((".b2nd", ".b2f")):
236+
if filepath.endswith((".b2nd", ".b2f", ".b2b")):
232237
# Convert filename to key: remove extension and ensure starts with /
233238
rel_path = os.path.relpath(filepath, self.working_dir)
234239
# Normalize path separators to forward slashes for cross-platform consistency
235240
rel_path = rel_path.replace(os.sep, "/")
236241
if rel_path.endswith(".b2nd"):
237242
key = rel_path[:-5]
238-
elif rel_path.endswith(".b2f"):
243+
elif rel_path.endswith(".b2b") or rel_path.endswith(".b2f"):
239244
key = rel_path[:-4]
240245
else:
241246
continue
@@ -264,6 +269,8 @@ def _is_external_value(value: blosc2.Array | SChunk | blosc2.VLArray | blosc2.Ba
264269
def _external_ext(value: blosc2.Array | SChunk | blosc2.VLArray | blosc2.BatchStore) -> str:
265270
if isinstance(value, blosc2.NDArray):
266271
return ".b2nd"
272+
if isinstance(value, blosc2.BatchStore):
273+
return ".b2b"
267274
return ".b2f"
268275

269276
def __setitem__(

src/blosc2/tree_store.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -668,8 +668,15 @@ def _persist_vlmeta(self) -> None:
668668
"""
669669
if hasattr(self, "_vlmeta_key"):
670670
vlmeta_key = self._vlmeta_key
671-
# Only embedded case is expected; handle it safely.
672-
if hasattr(self, "_estore") and vlmeta_key in self._estore:
671+
if vlmeta_key in self.map_tree:
672+
filepath = self.map_tree[vlmeta_key]
673+
dest_path = os.path.join(self.working_dir, filepath)
674+
parent_dir = os.path.dirname(dest_path)
675+
if parent_dir and not os.path.exists(parent_dir):
676+
os.makedirs(parent_dir, exist_ok=True)
677+
with open(dest_path, "wb") as f:
678+
f.write(self._vlmeta.to_cframe())
679+
elif hasattr(self, "_estore") and vlmeta_key in self._estore:
673680
# Replace the stored snapshot
674681
with contextlib.suppress(KeyError):
675682
del self._estore[vlmeta_key]

tests/test_tree_store.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,34 @@ def test_external_vlarray_support():
654654
os.remove("test_vlarray_external.b2z")
655655

656656

657+
def test_external_batchstore_support(tmp_path):
658+
store_path = tmp_path / "test_batchstore_external.b2d"
659+
660+
with TreeStore(str(store_path), mode="w", threshold=0) as tstore:
661+
bstore = blosc2.BatchStore(blocksize_max=2)
662+
bstore.extend([[{"id": 1}, {"id": 2}], [{"id": 3}]])
663+
tstore["/data/batchstore"] = bstore
664+
665+
batchstore_path = store_path / "data" / "batchstore.b2b"
666+
assert batchstore_path.exists()
667+
668+
with TreeStore(str(store_path), mode="r") as tstore:
669+
retrieved = tstore["/data/batchstore"]
670+
assert isinstance(retrieved, blosc2.BatchStore)
671+
assert [batch[:] for batch in retrieved] == [[{"id": 1}, {"id": 2}], [{"id": 3}]]
672+
673+
674+
def test_treestore_vlmeta_externalized_b2d(tmp_path):
675+
store_path = tmp_path / "test_vlmeta_externalized.b2d"
676+
677+
with TreeStore(str(store_path), mode="w", threshold=0) as tstore:
678+
tstore["/data"] = np.array([1, 2, 3])
679+
tstore.vlmeta["schema_manifest"] = {"version": 1, "fields": {"a": {"kind": "fixed"}}}
680+
681+
with TreeStore(str(store_path), mode="r") as tstore:
682+
assert tstore.vlmeta["schema_manifest"] == {"version": 1, "fields": {"a": {"kind": "fixed"}}}
683+
684+
657685
def test_walk_topdown_argument_ordering():
658686
"""Ensure walk supports topdown argument mimicking os.walk order semantics."""
659687
with TreeStore("test_walk_topdown.b2z", mode="w") as tstore:

0 commit comments

Comments
 (0)