Skip to content

Commit b21f7f4

Browse files
committed
Zarr: optimize appending
1 parent dcf2ac4 commit b21f7f4

File tree

2 files changed

+73
-65
lines changed

2 files changed

+73
-65
lines changed

xarray/backends/api.py

+2-54
Original file line numberDiff line numberDiff line change
@@ -1521,42 +1521,6 @@ def save_mfdataset(
15211521
)
15221522

15231523

1524-
def _validate_datatypes_for_zarr_append(zstore, dataset):
1525-
"""If variable exists in the store, confirm dtype of the data to append is compatible with
1526-
existing dtype.
1527-
"""
1528-
1529-
existing_vars = zstore.get_variables()
1530-
1531-
def check_dtype(vname, var):
1532-
if (
1533-
vname not in existing_vars
1534-
or np.issubdtype(var.dtype, np.number)
1535-
or np.issubdtype(var.dtype, np.datetime64)
1536-
or np.issubdtype(var.dtype, np.bool_)
1537-
or var.dtype == object
1538-
):
1539-
# We can skip dtype equality checks under two conditions: (1) if the var to append is
1540-
# new to the dataset, because in this case there is no existing var to compare it to;
1541-
# or (2) if var to append's dtype is known to be easy-to-append, because in this case
1542-
# we can be confident appending won't cause problems. Examples of dtypes which are not
1543-
# easy-to-append include length-specified strings of type `|S*` or `<U*` (where * is a
1544-
# positive integer character length). For these dtypes, appending dissimilar lengths
1545-
# can result in truncation of appended data. Therefore, variables which already exist
1546-
# in the dataset, and with dtypes which are not known to be easy-to-append, necessitate
1547-
# exact dtype equality, as checked below.
1548-
pass
1549-
elif not var.dtype == existing_vars[vname].dtype:
1550-
raise ValueError(
1551-
f"Mismatched dtypes for variable {vname} between Zarr store on disk "
1552-
f"and dataset to append. Store has dtype {existing_vars[vname].dtype} but "
1553-
f"dataset to append has dtype {var.dtype}."
1554-
)
1555-
1556-
for vname, var in dataset.data_vars.items():
1557-
check_dtype(vname, var)
1558-
1559-
15601524
# compute=True returns ZarrStore
15611525
@overload
15621526
def to_zarr(
@@ -1712,7 +1676,7 @@ def to_zarr(
17121676

17131677
if region is not None:
17141678
zstore._validate_and_autodetect_region(dataset)
1715-
# can't modify indexed with region writes
1679+
# can't modify indexes with region writes
17161680
dataset = dataset.drop_vars(dataset.indexes)
17171681
if append_dim is not None and append_dim in region:
17181682
raise ValueError(
@@ -1721,28 +1685,12 @@ def to_zarr(
17211685
)
17221686

17231687
if mode in ["a", "a-", "r+"]:
1724-
_validate_datatypes_for_zarr_append(zstore, dataset)
1725-
if append_dim is not None:
1726-
existing_dims = zstore.get_dimensions()
1727-
if append_dim not in existing_dims:
1728-
raise ValueError(
1729-
f"append_dim={append_dim!r} does not match any existing "
1730-
f"dataset dimensions {existing_dims}"
1731-
)
17321688
existing_var_names = set(zstore.zarr_group.array_keys())
17331689
for var_name in existing_var_names:
1734-
if var_name in encoding.keys():
1690+
if var_name in encoding:
17351691
raise ValueError(
17361692
f"variable {var_name!r} already exists, but encoding was provided"
17371693
)
1738-
if mode == "r+":
1739-
new_names = [k for k in dataset.variables if k not in existing_var_names]
1740-
if new_names:
1741-
raise ValueError(
1742-
f"dataset contains non-pre-existing variables {new_names}, "
1743-
"which is not allowed in ``xarray.Dataset.to_zarr()`` with "
1744-
"mode='r+'. To allow writing new variables, set mode='a'."
1745-
)
17461694

17471695
writer = ArrayWriter()
17481696
# TODO: figure out how to properly handle unlimited_dims

xarray/backends/zarr.py

+71-11
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,34 @@ def encode_zarr_variable(var, needs_copy=True, name=None):
324324
return var
325325

326326

327+
def _validate_datatypes_for_zarr_append(vname, existing_var, new_var):
328+
"""If variable exists in the store, confirm dtype of the data to append is compatible with
329+
existing dtype.
330+
"""
331+
if (
332+
np.issubdtype(new_var.dtype, np.number)
333+
or np.issubdtype(new_var.dtype, np.datetime64)
334+
or np.issubdtype(new_var.dtype, np.bool_)
335+
or new_var.dtype == object
336+
):
337+
# We can skip dtype equality checks under two conditions: (1) if the var to append is
338+
# new to the dataset, because in this case there is no existing var to compare it to;
339+
# or (2) if var to append's dtype is known to be easy-to-append, because in this case
340+
# we can be confident appending won't cause problems. Examples of dtypes which are not
341+
# easy-to-append include length-specified strings of type `|S*` or `<U*` (where * is a
342+
# positive integer character length). For these dtypes, appending dissimilar lengths
343+
# can result in truncation of appended data. Therefore, variables which already exist
344+
# in the dataset, and with dtypes which are not known to be easy-to-append, necessitate
345+
# exact dtype equality, as checked below.
346+
pass
347+
elif not new_var.dtype == existing_var.dtype:
348+
raise ValueError(
349+
f"Mismatched dtypes for variable {vname} between Zarr store on disk "
350+
f"and dataset to append. Store has dtype {existing_var.dtype} but "
351+
f"dataset to append has dtype {new_var.dtype}."
352+
)
353+
354+
327355
def _validate_and_transpose_existing_dims(
328356
var_name, new_var, existing_var, region, append_dim
329357
):
@@ -612,26 +640,58 @@ def store(
612640
import zarr
613641

614642
existing_keys = tuple(self.zarr_group.array_keys())
643+
644+
if self._mode == "r+":
645+
new_names = [k for k in variables if k not in existing_keys]
646+
if new_names:
647+
raise ValueError(
648+
f"dataset contains non-pre-existing variables {new_names}, "
649+
"which is not allowed in ``xarray.Dataset.to_zarr()`` with "
650+
"``mode='r+'``. To allow writing new variables, set ``mode='a'``."
651+
)
652+
653+
if self._append_dim is not None and self._append_dim not in existing_keys:
654+
# For dimensions without coordinate values, we must parse
655+
# the _ARRAY_DIMENSIONS attribute on *all* arrays to check if it
656+
# is a valid existing dimension name.
657+
# TODO: This `get_dimensions` method also does shape checking
658+
# which isn't strictly necessary for our check.
659+
existing_dims = self.get_dimensions()
660+
if self._append_dim not in existing_dims:
661+
raise ValueError(
662+
f"append_dim={self._append_dim!r} does not match any existing "
663+
f"dataset dimensions {existing_dims}"
664+
)
665+
615666
existing_variable_names = {
616667
vn for vn in variables if _encode_variable_name(vn) in existing_keys
617668
}
618-
new_variables = set(variables) - existing_variable_names
619-
variables_without_encoding = {vn: variables[vn] for vn in new_variables}
669+
new_variable_names = set(variables) - existing_variable_names
620670
variables_encoded, attributes = self.encode(
621-
variables_without_encoding, attributes
671+
{vn: variables[vn] for vn in new_variable_names}, attributes
622672
)
623673

624674
if existing_variable_names:
625-
# Decode variables directly, without going via xarray.Dataset to
626-
# avoid needing to load index variables into memory.
627-
# TODO: consider making loading indexes lazy again?
675+
# We make sure that values to be appended are encoded *exactly*
676+
# as the current values in the store.
677+
# To do so, we decode variables directly to access the proper encoding,
678+
# without going via xarray.Dataset to avoid needing to load
679+
# index variables into memory.
628680
existing_vars, _, _ = conventions.decode_cf_variables(
629-
{k: self.open_store_variable(name=k) for k in existing_variable_names},
630-
self.get_attrs(),
681+
variables={
682+
k: self.open_store_variable(name=k) for k in existing_variable_names
683+
},
684+
# attributes = {} since we don't care about parsing the global
685+
# "coordinates" attribute
686+
attributes={},
631687
)
632688
# Modified variables must use the same encoding as the store.
633689
vars_with_encoding = {}
634690
for vn in existing_variable_names:
691+
if self._mode in ["a", "a-", "r+"]:
692+
_validate_datatypes_for_zarr_append(
693+
vn, existing_vars[vn], variables[vn]
694+
)
635695
vars_with_encoding[vn] = variables[vn].copy(deep=False)
636696
vars_with_encoding[vn].encoding = existing_vars[vn].encoding
637697
vars_with_encoding, _ = self.encode(vars_with_encoding, {})
@@ -696,7 +756,7 @@ def set_variables(self, variables, check_encoding_set, writer, unlimited_dims=No
696756

697757
for vn, v in variables.items():
698758
name = _encode_variable_name(vn)
699-
check = vn in check_encoding_set
759+
700760
attrs = v.attrs.copy()
701761
dims = v.dims
702762
dtype = v.dtype
@@ -712,7 +772,7 @@ def set_variables(self, variables, check_encoding_set, writer, unlimited_dims=No
712772
# https://github.com/pydata/xarray/issues/8371 for details.
713773
encoding = extract_zarr_variable_encoding(
714774
v,
715-
raise_on_invalid=check,
775+
raise_on_invalid=vn in check_encoding_set,
716776
name=vn,
717777
safe_chunks=self._safe_chunks,
718778
)
@@ -815,7 +875,7 @@ def _auto_detect_regions(self, ds, region):
815875
assert variable.dims == (dim,)
816876
index = pd.Index(variable.data)
817877
idxs = index.get_indexer(ds[dim].data)
818-
if any(idxs == -1):
878+
if (idxs == -1).any():
819879
raise KeyError(
820880
f"Not all values of coordinate '{dim}' in the new array were"
821881
" found in the original store. Writing to a zarr region slice"

0 commit comments

Comments
 (0)