Skip to content

Commit 49f6a0b

Browse files
author
Ivan Dlugos
committed
Box bulk (many) put & get
1 parent baa3c3a commit 49f6a0b

5 files changed

Lines changed: 166 additions & 26 deletions

File tree

objectbox/box.py

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,33 +22,104 @@ def count(self, limit: int = 0) -> int:
2222
obx_box_count(self._c_box, limit, ctypes.byref(count))
2323
return int(count.value)
2424

25-
def put(self, object) -> int:
26-
id = object_id = self._entity.get_object_id(object)
25+
def put(self, *objects):
26+
"""Puts an object (or a list of objects) and returns its ID (or nothing for a list objects)"""
27+
28+
if len(objects) != 1:
29+
self._put_many(objects)
30+
elif isinstance(objects[0], list):
31+
self._put_many(objects[0])
32+
else:
33+
return self._put_one(objects[0])
34+
35+
def _put_one(self, obj) -> int:
36+
id = object_id = self._entity.get_object_id(obj)
2737

2838
if not id:
2939
id = obx_box_id_for_put(self._c_box, 0)
3040

31-
data = self._entity.marshal(object, id)
41+
data = self._entity.marshal(obj, id)
3242
obx_box_put(self._c_box, id, bytes(data), len(data), OBXPutMode_PUT)
3343

3444
if id != object_id:
35-
self._entity.set_object_id(object, id)
45+
self._entity.set_object_id(obj, id)
46+
3647
return id
3748

49+
def _put_many(self, objects) -> None:
50+
# retrieve IDs from the objects (to distinguish new objects and updates)
51+
new = {}
52+
ids = {}
53+
for k in range(len(objects)):
54+
id = self._entity.get_object_id(objects[k])
55+
if not id:
56+
new[k] = 0
57+
ids[k] = id
58+
59+
# acquire IDs for the new objects and set them
60+
if len(new) > 0:
61+
c_next_id = obx_id()
62+
obx_box_ids_for_put(self._c_box, len(new), ctypes.byref(c_next_id))
63+
next_id = c_next_id.value
64+
for k in new.keys():
65+
ids[k] = next_id
66+
next_id += 1
67+
68+
# allocate a C bytes array structure where we will push the object data
69+
# OBX_bytes_array with .count = len(objects)
70+
c_bytes_array_p = obx_bytes_array_create(len(objects))
71+
72+
try:
73+
# we need to keep the data around until put_many is executed because obx_bytes_array_set doesn't do a copy
74+
data = {}
75+
for k in range(len(objects)):
76+
data[k] = bytes(self._entity.marshal(objects[k], ids[k]))
77+
key = ctypes.c_size_t(k)
78+
79+
# OBX_bytes_array.data[k] = data
80+
obx_bytes_array_set(c_bytes_array_p, key, data[k], len(data[k]))
81+
82+
c_ids = (obx_id * len(ids))(*ids.values())
83+
obx_box_put_many(self._c_box, c_bytes_array_p, c_ids, OBXPutMode_PUT)
84+
85+
finally:
86+
obx_bytes_array_free(c_bytes_array_p)
87+
88+
# assign new IDs on the object
89+
for k in new.keys():
90+
self._entity.set_object_id(objects[k], ids[k])
91+
3892
def get(self, id: int):
3993
c_data = ctypes.c_void_p()
4094
c_size = ctypes.c_size_t()
4195
obx_box_get(self._c_box, id, ctypes.byref(c_data), ctypes.byref(c_size))
4296

43-
# TODO verify which of the following two approaches is better.
97+
data = c_voidp_as_bytes(c_data, c_size.value)
98+
return self._entity.unmarshal(data)
4499

45-
# slice the data from the pointer
46-
# data = ctypes.cast(c_data, ctypes.POINTER(ctypes.c_char))[:c_size.value]
100+
def get_all(self) -> list:
101+
# OBX_bytes_array*
102+
c_bytes_array_p = obx_box_get_all(self._c_box)
47103

48-
# create a memory view
49-
data = memoryview(ctypes.cast(c_data, ctypes.POINTER(ctypes.c_ubyte * c_size.value))[0]).tobytes()
104+
try:
105+
# OBX_bytes_array
106+
c_bytes_array = c_bytes_array_p.contents
50107

51-
return self._entity.unmarshal(data)
108+
result = list()
109+
for i in range(c_bytes_array.count):
110+
# OBX_bytes
111+
c_bytes = c_bytes_array.data[i]
112+
data = c_voidp_as_bytes(c_bytes.data, c_bytes.size)
113+
result.append(self._entity.unmarshal(data))
114+
115+
return result
116+
finally:
117+
obx_bytes_array_free(c_bytes_array_p)
52118

53119
def remove(self, id: int):
54-
obx_box_remove(self._c_box, id)
120+
obx_box_remove(self._c_box, id)
121+
122+
def remove_all(self) -> int:
123+
count = ctypes.c_uint64()
124+
obx_box_remove_all(self._c_box, ctypes.byref(count))
125+
return int(count.value)

objectbox/c.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class OBX_bytes(ctypes.Structure):
6161

6262
class OBX_bytes_array(ctypes.Structure):
6363
_fields_ = [
64-
('data', ctypes.POINTER(OBX_bytes)),
64+
('data', OBX_bytes_p),
6565
('count', ctypes.c_size_t),
6666
]
6767

@@ -194,6 +194,18 @@ def c_str(string: str) -> ctypes.c_char_p:
194194
return string.encode('utf-8')
195195

196196

197+
def c_voidp_as_bytes(voidp, size):
198+
# TODO verify which of the following two approaches is better. Performance-wise, it seems the same.
199+
200+
# slice the data from the pointer
201+
# return ctypes.cast(voidp, ctypes.POINTER(ctypes.c_char))[:size]
202+
203+
# create a memory view
204+
return memoryview(ctypes.cast(voidp, ctypes.POINTER(ctypes.c_ubyte * size))[0]).tobytes()
205+
206+
207+
208+
197209
# OBX_model* (void);
198210
obx_model_create = fn('obx_model_create', OBX_model_p, [])
199211

@@ -233,21 +245,43 @@ def c_str(string: str) -> ctypes.c_char_p:
233245
obx_box_get = fn('obx_box_get', obx_err,
234246
[OBX_box_p, obx_id, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_size_t)])
235247

248+
# OBX_bytes_array* (OBX_box* box);
249+
obx_box_get_all = fn('obx_box_get_all', OBX_bytes_array_p, [OBX_box_p])
250+
236251
# obx_id (OBX_box* box, obx_id id_or_zero);
237252
obx_box_id_for_put = fn('obx_box_id_for_put', obx_id, [OBX_box_p, obx_id])
238253

254+
# obx_err (OBX_box* box, uint64_t count, obx_id* out_first_id);
255+
obx_box_ids_for_put = fn('obx_box_ids_for_put', obx_err, [OBX_box_p, ctypes.c_uint64, ctypes.POINTER(obx_id)])
256+
239257
# obx_err (OBX_box* box, obx_id id, const void* data, size_t size, OBXPutMode mode);
240258
obx_box_put = fn('obx_box_put', obx_err, [OBX_box_p, obx_id, ctypes.c_void_p, ctypes.c_size_t, OBXPutMode])
241259

260+
# obx_err (OBX_box* box, const OBX_bytes_array* objects, const obx_id* ids, OBXPutMode mode);
261+
obx_box_put_many = fn('obx_box_put_many', obx_err, [OBX_box_p, OBX_bytes_array_p, ctypes.POINTER(obx_id), OBXPutMode])
262+
242263
# obx_err (OBX_box* box, obx_id id);
243264
obx_box_remove = fn('obx_box_remove', obx_err, [OBX_box_p, obx_id])
244265

266+
# obx_err (OBX_box* box, uint64_t* out_count);
267+
obx_box_remove_all = fn('obx_box_remove_all', obx_err, [OBX_box_p, ctypes.POINTER(ctypes.c_uint64)])
268+
245269
# obx_err (OBX_box* box, bool* out_is_empty);
246270
obx_box_is_empty = fn('obx_box_is_empty', obx_err, [OBX_box_p, ctypes.POINTER(ctypes.c_bool)])
247271

248272
# obx_err obx_box_count(OBX_box* box, uint64_t limit, uint64_t* out_count);
249273
obx_box_count = fn('obx_box_count', obx_err, [OBX_box_p, ctypes.c_uint64, ctypes.POINTER(ctypes.c_uint64)])
250274

275+
# OBX_bytes_array* (size_t count);
276+
obx_bytes_array_create = fn('obx_bytes_array_create', OBX_bytes_array_p, [ctypes.c_size_t])
277+
278+
# obx_err (OBX_bytes_array* array, size_t index, const void* data, size_t size);
279+
obx_bytes_array_set = fn('obx_bytes_array_set', obx_err,
280+
[OBX_bytes_array_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t])
281+
282+
# void (OBX_bytes_array * array);
283+
obx_bytes_array_free = fn('obx_bytes_array_free', None, [OBX_bytes_array_p])
284+
251285
OBXPropertyType_Bool = 1
252286
OBXPropertyType_Byte = 2
253287
OBXPropertyType_Short = 3

objectbox/model/entity.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ def __init__(self, cls, id: int, uid: int):
2626
self.id_property: Property = None
2727
self.fill_properties()
2828

29-
def __call__(self):
30-
return self.cls()
29+
def __call__(self, *args):
30+
return self.cls(*args)
3131

3232
def fill_properties(self):
3333
# TODO allow subclassing and support entities with __slots__ defined

tests/model.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ class TestEntity:
1010
float = Property(float, id=5, uid=1005)
1111
bytes = Property(bytes, id=6, uid=1006)
1212
transient: str = "" # not "Property" so it's not stored
13+
14+
def __init__(self, string: str = ""):
15+
self.str = string

tests/test_basics.py

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,17 @@ def test_open():
2929
load_empty_test_objectbox()
3030

3131

32-
def test_box():
32+
def assert_equal(actual, expected):
33+
"""Check that two TestEntity objects have the same property data"""
34+
assert actual.id == expected.id
35+
assert isinstance(expected.bool, objectbox.model.Property) or actual.bool == expected.bool
36+
assert isinstance(expected.int, objectbox.model.Property) or actual.int == expected.int
37+
assert isinstance(expected.str, objectbox.model.Property) or actual.str == expected.str
38+
assert isinstance(expected.float, objectbox.model.Property) or actual.float == expected.float
39+
assert isinstance(expected.bytes, objectbox.model.Property) or actual.bytes == expected.bytes
40+
41+
42+
def test_box_basics():
3343
ob = load_empty_test_objectbox()
3444
box = objectbox.Box(ob, TestEntity)
3545

@@ -60,18 +70,10 @@ def test_box():
6070
assert not box.is_empty()
6171
assert box.count() == 2
6272

63-
def verifyObject(read, object):
64-
assert read.id == object.id
65-
assert read.bool == object.bool
66-
assert read.int == object.int
67-
assert read.str == object.str
68-
assert read.float == object.float
69-
assert read.bytes == object.bytes
70-
assert read.transient != object.transient # !=
71-
7273
# read
7374
read = box.get(object.id)
74-
verifyObject(read, object)
75+
assert_equal(read, object)
76+
assert read.transient != object.transient # !=
7577

7678
# update
7779
object.str = "bar"
@@ -80,7 +82,7 @@ def verifyObject(read, object):
8082

8183
# read again
8284
read = box.get(object.id)
83-
verifyObject(read, object)
85+
assert_equal(read, object)
8486

8587
# remove
8688
box.remove(object.id)
@@ -91,3 +93,33 @@ def verifyObject(read, object):
9193
box.get(object.id)
9294

9395

96+
def test_box_bulk():
97+
ob = load_empty_test_objectbox()
98+
box = objectbox.Box(ob, TestEntity)
99+
100+
box.put(TestEntity("first"))
101+
102+
objects = [TestEntity("second"), TestEntity("third"), TestEntity("fourth"), box.get(1)]
103+
box.put(objects)
104+
assert box.count() == 4
105+
assert objects[0].id == 2
106+
assert objects[1].id == 3
107+
assert objects[2].id == 4
108+
assert objects[3].id == 1
109+
110+
assert_equal(box.get(objects[0].id), objects[0])
111+
assert_equal(box.get(objects[1].id), objects[1])
112+
assert_equal(box.get(objects[2].id), objects[2])
113+
assert_equal(box.get(objects[3].id), objects[3])
114+
115+
objects_read = box.get_all()
116+
assert len(objects_read) == 4
117+
assert_equal(objects_read[0], objects[3])
118+
assert_equal(objects_read[1], objects[0])
119+
assert_equal(objects_read[2], objects[1])
120+
assert_equal(objects_read[3], objects[2])
121+
122+
# remove all
123+
removed = box.remove_all()
124+
assert removed == 4
125+
assert box.count() == 0

0 commit comments

Comments
 (0)