Skip to content

Commit f5c9f33

Browse files
committed
add update_value option to queryDataset
1 parent 123fb5f commit f5c9f33

3 files changed

Lines changed: 161 additions & 14 deletions

File tree

src/h5json/h5writer.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,18 @@ def append(self):
7070
def no_data(self):
7171
return self._no_data
7272

73+
def queryDataset(self, obj_id, query, sel=None, limit=0, update_value=None):
74+
"""
75+
Replace any elements that match the
76+
77+
Return a numpy array of indices for the elements that match the query.
78+
Writers are not required to implement this — by default it raises
79+
NotImplementedError, and Hdf5db falls back to querying the dataset values
80+
it fetches via getDatasetValues. Override this only if the storage backend
81+
has a more efficient way to evaluate the query (e.g. pushing it down to storage).
82+
"""
83+
raise NotImplementedError("queryDataset not implemented for " + type(self).__name__)
84+
7385
@abstractmethod
7486
def open(self):
7587
""" open storage handle, return root_id"""

src/h5json/hdf5db.py

Lines changed: 104 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ def _query_rel_to_abs(x_sel, rel_indices, rank):
3535
"""
3636
slices = x_sel.slices
3737
if len(rel_indices) == 0:
38-
return np.zeros((0, rank), dtype='u8')
39-
abs_result = np.zeros((len(rel_indices), rank), dtype='u8')
38+
return np.zeros((0, rank), dtype='int64')
39+
abs_result = np.zeros((len(rel_indices), rank), dtype='int64')
4040
for d in range(rank):
4141
s = slices[d]
4242
if isinstance(s, slice):
@@ -1110,13 +1110,16 @@ def getChunkIterator(self, dset_id, sel=None):
11101110
"""
11111111
return ChunkIterator(self, dset_id, sel=sel)
11121112

1113-
def queryDataset(self, dset_id, query, sel=None, limit=0):
1113+
def queryDataset(self, dset_id, query, sel=None, limit=0, update_value=None):
11141114
"""
11151115
Query the given dataset using the selection and query expression
11161116
If sel is provided, only the elements in the selection will be queried,
11171117
otherwise the entire dataset will be queried.
11181118
If limit is provided, only the first limit number of elements that match the query will be returned.
1119-
1119+
If update_value is provied, elements matching the query (up to limit elements if limit is non-zero)
1120+
will be updated to the given value. For a compound dtype, update_value may be a dict mapping
1121+
field names to the value to set for that field - only those fields are modified, and the rest
1122+
of each matching element is left unchanged.
11201123
Return a numpy array of indices for the elements that match the query
11211124
"""
11221125

@@ -1155,10 +1158,75 @@ def queryReader(dset_id, query, sel=None, limit=0):
11551158
if limit > 0 and nhits >= limit:
11561159
break
11571160

1158-
result = np.concatenate(hits, axis=0) if hits else np.zeros((0, rank), dtype='u8')
1161+
result = np.concatenate(hits, axis=0) if hits else np.zeros((0, rank), dtype='int64')
11591162
if limit > 0 and len(result) > limit:
11601163
result = result[:limit]
11611164
return result
1165+
1166+
def queryWriter(dset_id, query, sel=None, limit=0, update_value=None):
1167+
result = None
1168+
try:
1169+
result = self.writer.queryDataset(dset_id, query, sel=sel, limit=limit, update_value=update_value)
1170+
except NotImplementedError:
1171+
# This reader doesn't support queryDataset
1172+
pass
1173+
1174+
if result is None:
1175+
rank = len(sel.shape)
1176+
try:
1177+
chunk_iter = ChunkIterator(self, dset_id, sel=sel)
1178+
except ValueError:
1179+
# ChunkIterator doesn't support this selection (e.g. a fancy/point
1180+
# selection, or a scalar dataset) - fall back to querying the
1181+
# entire selection at once
1182+
arr = self.getDatasetValues(dset_id, sel)
1183+
result = arrayQuery(query, arr, limit=limit)
1184+
result = _query_rel_to_abs(sel, result, rank)
1185+
else:
1186+
# query the dataset chunk by chunk so the whole selection is
1187+
# never loaded into memory at once
1188+
hits = []
1189+
nhits = 0
1190+
for chunk_arr in chunk_iter:
1191+
chunk_rel = arrayQuery(query, chunk_arr)
1192+
if len(chunk_rel) == 0:
1193+
continue
1194+
hits.append(_query_rel_to_abs(chunk_iter.sel, chunk_rel, rank))
1195+
nhits += len(chunk_rel)
1196+
if limit > 0 and nhits >= limit:
1197+
break
1198+
1199+
result = np.concatenate(hits, axis=0) if hits else np.zeros((0, rank), dtype='int64')
1200+
if limit > 0 and len(result) > limit:
1201+
result = result[:limit]
1202+
1203+
if update_value is not None and len(result) > 0:
1204+
# update the values at the matching indices
1205+
dtype = self.getDtype(self.getObjectById(dset_id))
1206+
if isinstance(update_value, dict):
1207+
# a dict maps field names to the value to set for that field -
1208+
# only those fields are modified, the rest of each record is
1209+
# left as-is
1210+
if len(dtype) == 0:
1211+
raise TypeError("update_value dict is only supported for compound dtypes")
1212+
fields = [f for f in dtype.names if f in update_value]
1213+
if not fields:
1214+
raise ValueError(
1215+
f"None of the requested fields {list(update_value.keys())} found in dtype")
1216+
if len(fields) == 1:
1217+
value = np.asarray(update_value[fields[0]], dtype=dtype.fields[fields[0]][0])
1218+
else:
1219+
value_dtype = np.dtype([(f, dtype.fields[f][0]) for f in fields])
1220+
value = np.zeros((), dtype=value_dtype)
1221+
for f in fields:
1222+
value[f] = update_value[f]
1223+
update_sel = selections.select(sel.shape, result, fields=fields)
1224+
else:
1225+
value = np.asarray(update_value, dtype=dtype)
1226+
update_sel = selections.select(sel.shape, result)
1227+
self.setDatasetValues(dset_id, update_sel, value)
1228+
return result
1229+
11621230
#
11631231
# start of queryDataset
11641232
#
@@ -1180,6 +1248,12 @@ def queryReader(dset_id, query, sel=None, limit=0):
11801248
if sel is None:
11811249
sel = selections.select(dims, ...)
11821250

1251+
if update_value is not None:
1252+
# do flush so we can be sure to do an atomic operation if the writer supports it
1253+
self.flush()
1254+
results = queryWriter(dset_id, query, sel=sel, limit=limit, update_value=update_value)
1255+
return results
1256+
11831257
updates = self._getDatasetUpdates(dset_id)
11841258

11851259
if sel.shape != dims:
@@ -1213,13 +1287,25 @@ def queryReader(dset_id, query, sel=None, limit=0):
12131287
result_mask &= ~inter_mask
12141288

12151289
# Query the updated values at the intersection
1216-
local_sel = selections.translate(update_sel, x_sel)
1217-
x_vals = update_val[local_sel.slices]
1218-
x_rel = arrayQuery(query, x_vals)
1219-
1220-
if len(x_rel) > 0:
1221-
abs_result = _query_rel_to_abs(x_sel, x_rel, rank)
1222-
result_mask[tuple(abs_result[:, d].astype(int) for d in range(rank))] = True
1290+
if update_sel.select_type == selections.H5S_SEL_POINTS:
1291+
# update_val is 1-D, indexed by position in update_sel (not
1292+
# sliceable via translate(), which only handles hyperslabs)
1293+
x_points = list(selections._iter_points(x_sel))
1294+
if not x_points:
1295+
continue
1296+
upd_pt_to_idx = {pt: j for j, pt in enumerate(selections._iter_points(update_sel))}
1297+
x_vals = update_val[[upd_pt_to_idx[pt] for pt in x_points]]
1298+
x_rel = arrayQuery(query, x_vals)
1299+
if len(x_rel) > 0:
1300+
abs_coords = np.array([x_points[i] for i in x_rel[:, 0]], dtype='u8')
1301+
result_mask[tuple(abs_coords[:, d] for d in range(rank))] = True
1302+
else:
1303+
local_sel = selections.translate(update_sel, x_sel)
1304+
x_vals = update_val[local_sel.slices]
1305+
x_rel = arrayQuery(query, x_vals)
1306+
if len(x_rel) > 0:
1307+
abs_result = _query_rel_to_abs(x_sel, x_rel, rank)
1308+
result_mask[tuple(abs_result[:, d].astype(int) for d in range(rank))] = True
12231309

12241310
indices = np.argwhere(result_mask)
12251311
if limit > 0 and len(indices) > limit:
@@ -1276,7 +1362,11 @@ def setDatasetValues(self, dset_id, sel, arr):
12761362
raise TypeError(f"arr.dtype {src_dt} doesn't match expected dtype {expected_dt}")
12771363

12781364
if sel.select_type == selections.H5S_SEL_POINTS:
1279-
if sel.nselect != arr.shape[0]:
1365+
if arr.shape == ():
1366+
# broadcast the scalar to match the number of selected points, so
1367+
# the stored update value can be indexed like any other point update
1368+
arr = np.full(sel.mshape, arr[()], dtype=arr.dtype)
1369+
elif sel.nselect != arr.shape[0]:
12801370
raise TypeError("Selection shape does not match number of points")
12811371
elif sel.select_type == selections.H5S_SEL_FANCY:
12821372
if arr.shape != sel.mshape:
@@ -1289,7 +1379,7 @@ def setDatasetValues(self, dset_id, sel, arr):
12891379
raise TypeError("Selection shape does not match dataset shape")
12901380
# Allow scalar arrays when writing a field-restricted selection
12911381
# (the scalar will be broadcast to all selected positions).
1292-
if arr.shape != () or sel.fields is None:
1382+
if arr.shape != ():
12931383
if 0 < arr.ndim < len(dims):
12941384
# arr has fewer dims than the dataset rank (e.g. a 1-D array
12951385
# written to a slice of a 3-D dataset). Validate against

test/unit/hdf5db_test.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,19 @@ def test2DDataset(self):
709709
else:
710710
self.assertEqual(x, i * 10 + j)
711711

712+
# point selection write with broadcasting
713+
arr = np.array(42, dtype=dtype)
714+
db.setDatasetValues(dset_id, sel, arr)
715+
arr = db.getDatasetValues(dset_id, sel_all)
716+
for i in range(nrows):
717+
for j in range(ncols):
718+
x = arr[i, j]
719+
if i == j and i < 4:
720+
# these are the elements were set to 42 with the point write
721+
self.assertEqual(x, 42)
722+
else:
723+
self.assertEqual(x, i * 10 + j)
724+
712725
# test select all write
713726
arr = np.zeros(shape, dtype=dtype)
714727
arr[...] = 42
@@ -1073,6 +1086,18 @@ def testQuerySimpleType(self):
10731086
query = "_ > 10"
10741087
indices = db.queryDataset(dset_id, query)
10751088
self.assertEqual(indices.shape, (56, 2))
1089+
1090+
indices = db.queryDataset(dset_id, query, update_value=0)
1091+
self.assertEqual(indices.shape, (56, 2))
1092+
1093+
indices = db.queryDataset(dset_id, query)
1094+
self.assertEqual(indices.shape, (0, 2))
1095+
1096+
# query update with limit
1097+
query = "_ == 0"
1098+
indices = db.queryDataset(dset_id, query, limit=5, update_value=-99)
1099+
self.assertEqual(indices.shape, (5, 2))
1100+
10761101
db.close()
10771102

10781103
def testQueryDataset1D(self):
@@ -1138,6 +1163,16 @@ def testQueryDataset1D(self):
11381163
except ValueError:
11391164
pass
11401165

1166+
# query with update_value
1167+
indices = db.queryDataset(dset_id, query, update_value={"open": -999, "close": 999})
1168+
self.assertEqual(indices.shape, (4, 1))
1169+
sel = selections.select(shape, indices)
1170+
values = db.getDatasetValues(dset_id, sel)
1171+
for i in range(len(values)):
1172+
self.assertEqual(values[i]["open"], -999)
1173+
self.assertEqual(values[i]["close"], 999)
1174+
self.assertEqual(values[i]["symbol"], b"AAPL")
1175+
11411176
db.close()
11421177

11431178
def testQueryDataset2D(self):
@@ -1171,6 +1206,16 @@ def testQueryDataset2D(self):
11711206
for i, row in enumerate(indices):
11721207
self.assertEqual(tuple(int(x) for x in row), expected_indexes[i])
11731208

1209+
# query with update_value
1210+
indices = db.queryDataset(dset_id, query, update_value={"open": -999, "close": 999})
1211+
self.assertEqual(indices.shape, (4, 2))
1212+
sel = selections.select(shape, indices)
1213+
values = db.getDatasetValues(dset_id, sel)
1214+
for i in range(len(values)):
1215+
self.assertEqual(values[i]["open"], -999)
1216+
self.assertEqual(values[i]["close"], 999)
1217+
self.assertEqual(values[i]["symbol"], b"AAPL")
1218+
11741219
db.close()
11751220

11761221
def testChunkIterator1D(self):

0 commit comments

Comments
 (0)