Skip to content

Commit 1d34b7c

Browse files
committed
added Hdf5db.queryDataset method
1 parent 6e38ca9 commit 1d34b7c

6 files changed

Lines changed: 157 additions & 118 deletions

File tree

src/h5json/h5reader.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,26 @@ def getAttribute(self, obj_id, name, includeData=True):
7373
pass
7474

7575
@abstractmethod
76-
def getDatasetValues(self, obj_id, sel=None, dtype=None, query=None):
76+
def getDatasetValues(self, obj_id, sel=None, dtype=None):
7777
"""
7878
Get values from dataset identified by obj_id.
7979
If a slices list or tuple is provided, it should have the same
8080
number of elements as the rank of the dataset.
8181
"""
8282
pass
8383

84+
def queryDataset(self, obj_id, query, sel=None):
85+
"""
86+
Query the given dataset using the selection and query expression
87+
88+
Return a numpy array of indices for the elements that match the query.
89+
Readers are not required to implement this — by default it raises
90+
NotImplementedError, and Hdf5db falls back to querying the dataset values
91+
it fetches via getDatasetValues. Override this only if the storage backend
92+
has a more efficient way to evaluate the query (e.g. pushing it down to storage).
93+
"""
94+
raise NotImplementedError("queryDataset not implemented for " + type(self).__name__)
95+
8496
@abstractmethod
8597
def open(self):
8698
""" Open data source for reading """

src/h5json/hdf5db.py

Lines changed: 98 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,11 @@ def _query_rel_to_abs(x_sel, rel_indices, rank):
3131
"""Map arrayQuery relative indices (within a sub-array) to absolute dataset indices.
3232
3333
x_sel: SimpleSelection whose sub-array was queried
34-
rel_indices: arrayQuery result — (N,) for rank=1, (N, rank) for rank>1
34+
rel_indices: arrayQuery result, ndarray of shape (N, rank)
3535
"""
3636
slices = x_sel.slices
37-
if rank == 1:
38-
s = slices[0]
39-
if isinstance(s, slice):
40-
start = s.start if s.start is not None else 0
41-
step = s.step if s.step is not None else 1
42-
return (rel_indices * step + start).astype(np.dtype('u8'))
43-
if isinstance(s, list):
44-
return np.array(s, dtype='u8')[rel_indices.astype(int)]
45-
return np.full(len(rel_indices), int(s), dtype='u8')
4637
if len(rel_indices) == 0:
47-
return rel_indices.astype('u8')
38+
return np.zeros((0, rank), dtype='u8')
4839
abs_result = np.zeros((len(rel_indices), rank), dtype='u8')
4940
for d in range(rank):
5041
s = slices[d]
@@ -696,7 +687,7 @@ def deleteAttribute(self, obj_id, name):
696687

697688
self.make_dirty(obj_id)
698689

699-
def getDatasetValues(self, dset_id, sel, query=None):
690+
def getDatasetValues(self, dset_id, sel):
700691
"""
701692
Get values from dataset identified by obj_id.
702693
If a slices list or tuple is provided, it should have the same
@@ -796,8 +787,6 @@ def init_arr(rdtype, cpl):
796787
raise ValueError("Only SELECT_ALL selections are supported for scalar datasets")
797788
if sel.shape != ():
798789
raise ValueError("Selection shape does not match dataset shape")
799-
if query:
800-
raise ValueError("Query is not supported for scalar datasets")
801790
if updates:
802791
# for scalars the update has to be the requested value
803792
(update_sel, arr) = updates[-1]
@@ -816,55 +805,6 @@ def init_arr(rdtype, cpl):
816805
arr = None
817806
fetch = True
818807

819-
if query:
820-
full_shape = sel.shape
821-
rank = len(full_shape)
822-
result_mask = np.zeros(full_shape, dtype=bool)
823-
824-
# Delegate query to the reader when it has relevant data
825-
query_fetch = not (isinstance(self._reader, H5NullReader) or dset_id in self._new_objects)
826-
if query_fetch:
827-
for (update_sel, _) in updates:
828-
if selections.contained(sel, update_sel):
829-
query_fetch = False
830-
break
831-
832-
if query_fetch:
833-
reader_result = self.reader.getDatasetValues(dset_id, sel, dtype=dtype, query=query)
834-
if reader_result is not None and len(reader_result) > 0:
835-
if rank == 1:
836-
result_mask[reader_result.astype(int)] = True
837-
else:
838-
result_mask[tuple(reader_result[:, d].astype(int) for d in range(rank))] = True
839-
840-
for (update_sel, update_val) in updates:
841-
x_sel = selections.intersect(sel, update_sel)
842-
if x_sel.nselect == 0:
843-
continue
844-
845-
# Invalidate reader results overwritten by this update
846-
inter_mask = np.zeros(full_shape, dtype=bool)
847-
inter_mask[x_sel.slices] = True
848-
result_mask &= ~inter_mask
849-
850-
# Query the updated values at the intersection
851-
local_sel = selections.translate(update_sel, x_sel)
852-
x_vals = update_val[local_sel.slices]
853-
x_rel = arrayQuery(query, x_vals)
854-
855-
if len(x_rel) > 0:
856-
abs_result = _query_rel_to_abs(x_sel, x_rel, rank)
857-
if rank == 1:
858-
result_mask[abs_result.astype(int)] = True
859-
else:
860-
result_mask[tuple(abs_result[:, d].astype(int) for d in range(rank))] = True
861-
862-
indices = np.argwhere(result_mask)
863-
if rank == 1:
864-
return indices.reshape(-1).astype(np.dtype('u8'))
865-
else:
866-
return indices.astype(np.dtype('u8'))
867-
868808
# determine if we need to get data from the reader
869809
if isinstance(self._reader, H5NullReader) or dset_id in self._new_objects:
870810
fetch = False
@@ -981,6 +921,101 @@ def init_arr(rdtype, cpl):
981921

982922
return arr
983923

924+
def queryDataset(self, dset_id, query, sel=None, limit=0):
925+
"""
926+
Query the given dataset using the selection and query expression
927+
If sel is provided, only the elements in the selection will be queried,
928+
otherwise the entire dataset will be queried.
929+
If limit is provided, only the first limit number of elements that match the query will be returned.
930+
931+
Return a numpy array of indices for the elements that match the query
932+
"""
933+
934+
def queryReader(dset_id, query, sel=None, limit=0):
935+
result = None
936+
try:
937+
result = self.reader.queryDataset(dset_id, query, sel=sel, limit=limit)
938+
except NotImplementedError:
939+
print("This reader doesn't support queryDataset")
940+
941+
if result is not None:
942+
return result
943+
944+
# query the dataset by fetching the data and applying the query locally
945+
arr = self.getDatasetValues(dset_id, sel)
946+
947+
result = arrayQuery(query, arr, limit=limit)
948+
result = _query_rel_to_abs(sel, result, len(sel.shape))
949+
950+
return result
951+
#
952+
# start of queryDataset
953+
#
954+
if not isinstance(query, str):
955+
raise TypeError("Expected query string")
956+
957+
if sel is not None and not isinstance(sel, selections.Selection):
958+
raise TypeError("Expected Selection class")
959+
if not isinstance(limit, int) or limit < 0:
960+
raise TypeError("Expected non-negative integer for limit")
961+
962+
dset_json = self.getObjectById(dset_id)
963+
shape_json = dset_json["shape"]
964+
965+
shape_class = getShapeClass(shape_json)
966+
if shape_class == "H5S_NULL":
967+
raise ValueError("querying null space dataset not supported")
968+
dims = getShapeDims(shape_json)
969+
if sel is None:
970+
sel = selections.select(dims, ...)
971+
972+
updates = self._getDatasetUpdates(dset_id)
973+
974+
if sel.shape != dims:
975+
raise TypeError("Selection shape does not match dataset shape")
976+
977+
full_shape = sel.shape
978+
rank = len(full_shape)
979+
980+
# Delegate query to the reader when it has relevant data
981+
query_fetch = not (isinstance(self._reader, H5NullReader) or dset_id in self._new_objects)
982+
if query_fetch:
983+
for (update_sel, _) in updates:
984+
if selections.contained(sel, update_sel):
985+
query_fetch = False
986+
break
987+
988+
result_mask = np.zeros(full_shape, dtype=bool)
989+
if query_fetch:
990+
fetched = queryReader(dset_id, query, sel=sel, limit=limit)
991+
if len(fetched) > 0:
992+
result_mask[tuple(fetched[:, d].astype(int) for d in range(rank))] = True
993+
994+
for (update_sel, update_val) in updates:
995+
x_sel = selections.intersect(sel, update_sel)
996+
if x_sel.nselect == 0:
997+
continue
998+
999+
# Invalidate reader results overwritten by this update
1000+
inter_mask = np.zeros(full_shape, dtype=bool)
1001+
inter_mask[x_sel.slices] = True
1002+
result_mask &= ~inter_mask
1003+
1004+
# Query the updated values at the intersection
1005+
local_sel = selections.translate(update_sel, x_sel)
1006+
x_vals = update_val[local_sel.slices]
1007+
x_rel = arrayQuery(query, x_vals)
1008+
1009+
if len(x_rel) > 0:
1010+
abs_result = _query_rel_to_abs(x_sel, x_rel, rank)
1011+
result_mask[tuple(abs_result[:, d].astype(int) for d in range(rank))] = True
1012+
1013+
indices = np.argwhere(result_mask)
1014+
if limit > 0 and len(indices) > limit:
1015+
indices = indices[:limit]
1016+
1017+
return indices
1018+
9841019
def setDatasetValues(self, dset_id, sel, arr):
9851020
"""
9861021
Write the given ndarray to the dataset using the selection

src/h5json/query_util.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -377,11 +377,7 @@ def arrayQuery(
377377
378378
if limit is not 0, only up to limit indices will be returned
379379
380-
The return value will be an ndarray. If data_arr is one dimensionsal, the return value will be
381-
an array of shape (count,) where count is the number of matches of the query. Each element will be
382-
an index to the matching element. If data_arr is multi-dimensional, the array will have shape (count, rank),
383-
where count is the number of matching elements and rank is the rank of the data_arr. In this case, element [i,j]
384-
of the returned array will be the nth coordinate of the ith match of the query.
380+
The return value will be an ndarray. The array shape (count, rank) where rank is the number of array dimensions.
385381
386382
Example queries:
387383
"_ > 1.0" # match any array element with a value greater than 1.0
@@ -417,8 +413,4 @@ def arrayQuery(
417413
indices = np.argwhere(mask)
418414
if limit > 0:
419415
indices = indices[:limit]
420-
421-
if rank == 1:
422-
return indices.reshape(-1).astype(np.dtype('u8'))
423-
else:
424-
return indices.astype(np.dtype('u8'))
416+
return indices

src/h5json/selections.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,13 +197,13 @@ def from_query_result(shape, indices):
197197
"""Create a PointSelection from an arrayQuery result.
198198
199199
shape: full dataset shape tuple
200-
indices: ndarray of shape (N,) for 1D or (N, rank) for nD, as returned by arrayQuery
200+
indices: ndarray of shape (N, rank), as returned by arrayQuery
201201
"""
202202
rank = len(shape)
203203
if len(indices) == 0:
204204
return _empty_paired_sel(shape)
205205
if rank == 1:
206-
return select(shape, indices.astype(int).tolist())
206+
return select(shape, indices[:, 0].astype(int).tolist())
207207
coords = tuple(indices[:, d].astype(int).tolist() for d in range(rank))
208208
return select(shape, coords)
209209

test/unit/array_query_test.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def testArrayQueryNoneCompound(self):
6969
query = "_ > 1.0"
7070
result = arrayQuery(query, data_arr)
7171
self.assertTrue(isinstance(result, np.ndarray))
72-
self.assertEqual(result.dtype, np.dtype("u8"))
72+
self.assertEqual(result.dtype, np.dtype("int64"))
7373
self.assertEqual(len(result.shape), 2)
7474
self.assertEqual(result.shape[0], len(expected))
7575
self.assertEqual(result.shape[1], 2)
@@ -83,18 +83,18 @@ def testArrayQuery1D(self):
8383
query = "symbol == b'AAPL'"
8484
result = arrayQuery(query, data_arr)
8585
self.assertTrue(isinstance(result, np.ndarray))
86-
self.assertEqual(result.dtype, np.dtype("u8"))
87-
self.assertEqual(result.shape, (4,))
86+
self.assertEqual(result.dtype, np.dtype("int64"))
87+
self.assertEqual(result.shape, (4, 1))
8888
expected_indexes = (1, 4, 7, 10) # rows above with AAPL as symbol
8989
for i in range(4):
9090
item = result[i]
91-
self.assertTrue(item in expected_indexes)
91+
self.assertTrue(item[0] in expected_indexes)
9292

9393
# read just one row back
9494
result = arrayQuery(query, data_arr, limit=1)
9595
self.assertTrue(isinstance(result, np.ndarray))
9696
self.assertEqual(len(result), 1)
97-
index = result[0]
97+
index = result[0][0]
9898
self.assertEqual(index, 1)
9999

100100
# query with selection, no limit

0 commit comments

Comments
 (0)