objectbox-api 5.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- objectbox/__init__.py +145 -0
- objectbox/box.py +294 -0
- objectbox/builder.py +42 -0
- objectbox/c.py +1572 -0
- objectbox/condition.py +300 -0
- objectbox/exceptions.py +238 -0
- objectbox/experimental.py +78 -0
- objectbox/logger.py +18 -0
- objectbox/model/__init__.py +63 -0
- objectbox/model/codegen.py +399 -0
- objectbox/model/entity.py +490 -0
- objectbox/model/fast_builder.py +324 -0
- objectbox/model/idsync.py +401 -0
- objectbox/model/iduid.py +32 -0
- objectbox/model/model.py +158 -0
- objectbox/model/properties.py +865 -0
- objectbox/objectbox.py +24 -0
- objectbox/query.py +468 -0
- objectbox/query_builder.py +450 -0
- objectbox/store.py +400 -0
- objectbox/store_options.py +136 -0
- objectbox/sync.py +1101 -0
- objectbox/transaction.py +60 -0
- objectbox/utils.py +60 -0
- objectbox/version.py +39 -0
- objectbox_api-5.0.0.dist-info/METADATA +63 -0
- objectbox_api-5.0.0.dist-info/RECORD +30 -0
- objectbox_api-5.0.0.dist-info/WHEEL +5 -0
- objectbox_api-5.0.0.dist-info/licenses/LICENSE +202 -0
- objectbox_api-5.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# Copyright 2026 ObjectBox Ltd. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Experimental fast FlatBuffers builder for objects (see objectbox.experimental).
|
|
16
|
+
|
|
17
|
+
Produces the same bytes as _Entity._marshal_builder (the generic flatbuffers.Builder), but with the layout precomputed
|
|
18
|
+
once per entity: instead of ~60 Python calls per property, each object takes a few C-implemented calls (struct, join).
|
|
19
|
+
|
|
20
|
+
How the reference lays out an object (the Builder writes back to front; "off" counts the bytes written so far):
|
|
21
|
+
1. Strings and vectors (_offset_properties order); each is aligned relative to off (Builder.Prep).
|
|
22
|
+
2. The fields in _properties order, each aligned to its size; the vtable slot of a field is the off after it.
|
|
23
|
+
3. The offset to the vtable (soffset), then the vtable: size, object size, one entry per slot (trailing empty slots
|
|
24
|
+
trimmed). The vtable is never shared: the Builder is cleared for each object.
|
|
25
|
+
4. Padding to "minalign" (the largest alignment used, fixed per entity), then the offset to the table (root).
|
|
26
|
+
So everything after step 1 depends only on off modulo 8 after step 1: at most 8 layouts per entity, built lazily.
|
|
27
|
+
In memory (front to back) this gives: root, padding, vtable, soffset, the fields in reverse order, the strings and
|
|
28
|
+
vectors in reverse order; each item is followed by its alignment padding.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import struct
|
|
32
|
+
from array import array as py_array # not array: the name of a local (numpy array) below
|
|
33
|
+
|
|
34
|
+
from objectbox.c import *
|
|
35
|
+
from objectbox.utils import date_value_to_int
|
|
36
|
+
|
|
37
|
+
_uint32 = struct.Struct('<I')
|
|
38
|
+
|
|
39
|
+
# How vectors are marshalled by the reference (numpy dtype per property type; byte vectors are bytes).
|
|
40
|
+
# numpy and flatbuffers (which imports numpy) are imported only for entities with such properties (or flex).
|
|
41
|
+
_numpy_dtypes = {
|
|
42
|
+
OBXPropertyType_BoolVector: 'bool',
|
|
43
|
+
OBXPropertyType_ShortVector: 'int16',
|
|
44
|
+
OBXPropertyType_CharVector: 'uint16',
|
|
45
|
+
OBXPropertyType_IntVector: 'int32',
|
|
46
|
+
OBXPropertyType_LongVector: 'int64',
|
|
47
|
+
OBXPropertyType_FloatVector: 'float32',
|
|
48
|
+
OBXPropertyType_DoubleVector: 'float64',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Kinds of offset (variable size) properties and of fields
|
|
52
|
+
_STRING, _BYTES, _NUMPY, _FLEX, _INT8_LIST = range(5)
|
|
53
|
+
_FIELD_SCALAR, _FIELD_OFFSET, _FIELD_ID, _FIELD_DATE, _FIELD_BOOL = range(5)
|
|
54
|
+
|
|
55
|
+
_pads = [b'\0' * n for n in range(16)] # padding (zero bytes) by length
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _Layout:
|
|
59
|
+
"""Fixed part of an object (everything but strings and vectors) for one residue of off modulo 8."""
|
|
60
|
+
__slots__ = ('prefix', 'fields', 'offset_adjust')
|
|
61
|
+
|
|
62
|
+
def __init__(self, prefix: bytes, fields: struct.Struct, offset_adjust: list):
|
|
63
|
+
self.prefix = prefix # root, padding, vtable, soffset, padding: constant
|
|
64
|
+
self.fields = fields # the fields in memory order (reverse _properties order), incl. padding
|
|
65
|
+
self.offset_adjust = offset_adjust # per offset field (memory order): uoffset = off + adjust - var_off
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class FastBuilder:
|
|
69
|
+
"""Marshals objects of one entity; created after the entity's IDs are synced (they define the slots).
|
|
70
|
+
|
|
71
|
+
Null values: a property set to None (not just unset; Flex stores None as a value) is left out of the object
|
|
72
|
+
(vtable entry 0), as by the reference. Then the layout also depends on which fields are null; caching a layout
|
|
73
|
+
per null combination does not scale in general (2^32 combinations for 32 properties, times 8 residues).
|
|
74
|
+
So the 8 cached layouts are for objects without nulls; an object with a null value is marshalled again by
|
|
75
|
+
marshal_with_nulls(), with the layout for its nulls from a size-limited cache (frequent combinations), else
|
|
76
|
+
computed per object. Computing a layout per object is still ~3x faster than the reference (e.g. 3.2 µs instead
|
|
77
|
+
of 11.4 µs for an entity with 3 properties, Python 3.12), as the reference's time goes mostly to writing value
|
|
78
|
+
by value (~170 Python calls per object), not to computing the layout.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
_MAX_NULL_LAYOUTS = 256 # layouts cached for objects with nulls, per entity (keyed by residue and nulls)
|
|
82
|
+
|
|
83
|
+
def __init__(self, entity):
|
|
84
|
+
self._num_slots = entity._last_property_iduid.id
|
|
85
|
+
minalign = 4 # the soffset (and every string or vector) is aligned to 4
|
|
86
|
+
|
|
87
|
+
# Offset properties in creation order: (kind, name, property (= unset), default value, numpy dtype)
|
|
88
|
+
offset_props = []
|
|
89
|
+
self._alignments = {} # property name: alignment of a vector's data (numpy); for minalign with nulls
|
|
90
|
+
self._np = None # numpy, if there are numpy vectors
|
|
91
|
+
self._flex_builder = None # flatbuffers.flexbuffers.Builder, if there are flex properties
|
|
92
|
+
for prop in entity._offset_properties:
|
|
93
|
+
ob_type = prop._ob_type
|
|
94
|
+
dtype = None
|
|
95
|
+
if ob_type == OBXPropertyType_String:
|
|
96
|
+
kind, default = _STRING, ''
|
|
97
|
+
elif ob_type == OBXPropertyType_ByteVector:
|
|
98
|
+
kind, default = _INT8_LIST if prop._py_type == list else _BYTES, prop._py_type()
|
|
99
|
+
elif ob_type == OBXPropertyType_Flex:
|
|
100
|
+
kind, default = _FLEX, None
|
|
101
|
+
import flatbuffers.flexbuffers
|
|
102
|
+
self._flex_builder = flatbuffers.flexbuffers.Builder
|
|
103
|
+
else:
|
|
104
|
+
import numpy as np
|
|
105
|
+
self._np = np
|
|
106
|
+
kind, dtype = _NUMPY, np.dtype(_numpy_dtypes[ob_type])
|
|
107
|
+
default = np.array([]) if prop._is_ndarray else prop._py_type()
|
|
108
|
+
minalign = max(minalign, np.dtype(dtype).alignment)
|
|
109
|
+
self._alignments[prop.name] = np.dtype(dtype).alignment
|
|
110
|
+
offset_props.append((kind, prop.name, prop, default, dtype))
|
|
111
|
+
self._offset_props = offset_props
|
|
112
|
+
|
|
113
|
+
# Fields in _properties order (for the layout): (property, struct format char or None for an offset, width)
|
|
114
|
+
# and in memory order (for marshalling): (kind, name, property (= unset), value for unset or other data)
|
|
115
|
+
offset_index = {prop.name: i for i, (_, _, prop, _, _) in enumerate(offset_props)}
|
|
116
|
+
self._fields = []
|
|
117
|
+
fields_in_memory_order = []
|
|
118
|
+
for prop in entity._properties:
|
|
119
|
+
fb_format = prop._fb_format
|
|
120
|
+
width = struct.calcsize('<' + fb_format) # standard size, as FlatBuffers
|
|
121
|
+
minalign = max(minalign, width)
|
|
122
|
+
ob_type = prop._ob_type
|
|
123
|
+
if prop.name in offset_index:
|
|
124
|
+
self._fields.append((prop, None, 4))
|
|
125
|
+
fields_in_memory_order.append((_FIELD_OFFSET, None, None, offset_index[prop.name]))
|
|
126
|
+
continue
|
|
127
|
+
self._fields.append((prop, fb_format, width))
|
|
128
|
+
if prop is entity._id_property:
|
|
129
|
+
fields_in_memory_order.append((_FIELD_ID, None, None, None))
|
|
130
|
+
elif ob_type == OBXPropertyType_Date or ob_type == OBXPropertyType_DateNano:
|
|
131
|
+
multiplier = 1000 if ob_type == OBXPropertyType_Date else 1000000000
|
|
132
|
+
fields_in_memory_order.append((_FIELD_DATE, prop.name, prop, multiplier))
|
|
133
|
+
elif ob_type == OBXPropertyType_Bool:
|
|
134
|
+
fields_in_memory_order.append((_FIELD_BOOL, prop.name, prop, False))
|
|
135
|
+
else:
|
|
136
|
+
fields_in_memory_order.append((_FIELD_SCALAR, prop.name, prop, prop._py_type()))
|
|
137
|
+
fields_in_memory_order.reverse()
|
|
138
|
+
self._fields_in_memory_order = fields_in_memory_order
|
|
139
|
+
self._minalign = minalign
|
|
140
|
+
self._layouts = [None] * 8
|
|
141
|
+
self._null_layouts = {} # (residue, nulls): _Layout
|
|
142
|
+
# Properties that can be null, in _properties order (not the ID; Flex stores None as a value)
|
|
143
|
+
self._nullable_names = [prop.name for prop, _, _ in self._fields
|
|
144
|
+
if prop is not entity._id_property and prop._ob_type != OBXPropertyType_Flex]
|
|
145
|
+
|
|
146
|
+
def _layout(self, residue: int) -> _Layout:
|
|
147
|
+
# Thread-safe without a lock: concurrent first calls create equal layouts, one of them is kept
|
|
148
|
+
layout = self._layouts[residue]
|
|
149
|
+
if layout is None:
|
|
150
|
+
layout = self._layouts[residue] = self._create_layout(residue)
|
|
151
|
+
return layout
|
|
152
|
+
|
|
153
|
+
def _null_layout(self, residue: int, nulls: frozenset) -> _Layout:
|
|
154
|
+
key = (residue, nulls)
|
|
155
|
+
layout = self._null_layouts.get(key)
|
|
156
|
+
if layout is None:
|
|
157
|
+
layout = self._create_layout(residue, nulls)
|
|
158
|
+
if len(self._null_layouts) < self._MAX_NULL_LAYOUTS:
|
|
159
|
+
self._null_layouts[key] = layout
|
|
160
|
+
return layout
|
|
161
|
+
|
|
162
|
+
def _create_layout(self, residue: int, nulls: frozenset = frozenset()) -> _Layout:
|
|
163
|
+
"""Simulates the reference Builder from step 2 on (see module doc), starting at off = residue.
|
|
164
|
+
nulls: names of the properties left out (None)."""
|
|
165
|
+
off = residue
|
|
166
|
+
writes = [] # back to front: (struct format, offset field adjust or None)
|
|
167
|
+
slots = [0] * self._num_slots
|
|
168
|
+
minalign = self._minalign
|
|
169
|
+
if nulls: # as the reference's Builder.minalign: the largest alignment of what is written
|
|
170
|
+
minalign = 4
|
|
171
|
+
for prop, fmt, width in self._fields:
|
|
172
|
+
if prop.name not in nulls:
|
|
173
|
+
minalign = max(minalign, width, self._alignments.get(prop.name, 1))
|
|
174
|
+
for prop, fmt, width in self._fields:
|
|
175
|
+
if prop.name in nulls:
|
|
176
|
+
continue # no field, vtable slot 0
|
|
177
|
+
pad = -off & (width - 1)
|
|
178
|
+
off += pad
|
|
179
|
+
writes.append(('x' * pad, None))
|
|
180
|
+
if fmt is None: # uoffset = off (before placing) + 4 - var_off, with off relative to the real start
|
|
181
|
+
writes.append(('I', off + 4 - residue))
|
|
182
|
+
else:
|
|
183
|
+
writes.append((fmt, None))
|
|
184
|
+
off += width
|
|
185
|
+
slots[prop._fb_slot] = off
|
|
186
|
+
pad = -off & 3
|
|
187
|
+
off += pad + 4
|
|
188
|
+
object_offset = off
|
|
189
|
+
num_slots = self._num_slots
|
|
190
|
+
while num_slots > 0 and slots[num_slots - 1] == 0:
|
|
191
|
+
num_slots -= 1
|
|
192
|
+
vtable_size = (num_slots + 2) * 2
|
|
193
|
+
vtable = [vtable_size, object_offset - residue] + [object_offset - s if s else 0 for s in slots[:num_slots]]
|
|
194
|
+
off += vtable_size
|
|
195
|
+
pad_finish = -(off + 4) & (minalign - 1)
|
|
196
|
+
off += pad_finish
|
|
197
|
+
root = off + 4 - object_offset
|
|
198
|
+
|
|
199
|
+
# Memory order: reverse of the writes
|
|
200
|
+
prefix = (_uint32.pack(root) + _pads[pad_finish] + struct.pack('<%dH' % len(vtable), *vtable) +
|
|
201
|
+
struct.pack('<i', vtable_size) + _pads[pad])
|
|
202
|
+
fmt = '<'
|
|
203
|
+
offset_adjust = []
|
|
204
|
+
for f, adjust in reversed(writes):
|
|
205
|
+
fmt += f
|
|
206
|
+
if adjust is not None:
|
|
207
|
+
offset_adjust.append(adjust)
|
|
208
|
+
return _Layout(prefix, struct.Struct(fmt), offset_adjust)
|
|
209
|
+
|
|
210
|
+
def marshal_with_nulls(self, obj, id: int) -> bytes:
|
|
211
|
+
"""marshal() for an object with null values (properties set to None); see the class doc."""
|
|
212
|
+
nulls = frozenset(name for name in self._nullable_names if getattr(obj, name) is None)
|
|
213
|
+
return self.marshal(obj, id, nulls)
|
|
214
|
+
|
|
215
|
+
def marshal(self, obj, id: int, nulls: frozenset = None) -> bytes:
|
|
216
|
+
"""Returns the FlatBuffer of the given object, with the given ID (same bytes as _Entity._marshal_builder).
|
|
217
|
+
Thread-safe: all state is local; unlike the reference, this needs no thread-local builder.
|
|
218
|
+
nulls: None, or (from marshal_with_nulls) the names of the properties that are None; with None, the first
|
|
219
|
+
null value found restarts via marshal_with_nulls()."""
|
|
220
|
+
uint32_pack = _uint32.pack
|
|
221
|
+
pads = _pads
|
|
222
|
+
np = self._np
|
|
223
|
+
|
|
224
|
+
# Step 1: strings and vectors, in creation order. The parts are collected back to front (each part's
|
|
225
|
+
# padding, data, length) and reversed at the end, giving memory order.
|
|
226
|
+
off = 0
|
|
227
|
+
parts = []
|
|
228
|
+
var_offs = []
|
|
229
|
+
for kind, name, prop, default, dtype in self._offset_props:
|
|
230
|
+
val = getattr(obj, name)
|
|
231
|
+
if val is prop:
|
|
232
|
+
val = default
|
|
233
|
+
elif val is None and kind != _FLEX:
|
|
234
|
+
if nulls is None:
|
|
235
|
+
return self.marshal_with_nulls(obj, id)
|
|
236
|
+
var_offs.append(None) # null: no data
|
|
237
|
+
continue
|
|
238
|
+
if kind == _STRING:
|
|
239
|
+
data = val.encode('utf-8')
|
|
240
|
+
n = len(data)
|
|
241
|
+
pad = -(off + n + 1) & 3
|
|
242
|
+
parts.append(pads[pad + 1])
|
|
243
|
+
parts.append(data)
|
|
244
|
+
parts.append(uint32_pack(n))
|
|
245
|
+
off += pad + 1 + n + 4
|
|
246
|
+
elif kind == _NUMPY:
|
|
247
|
+
array = np.asarray(val, dtype=dtype) # as np.array in the reference, but no copy if not needed
|
|
248
|
+
if array.ndim > 1:
|
|
249
|
+
raise TypeError("multidimensional-ndarray passed to CreateNumpyVector")
|
|
250
|
+
if array.dtype.str[0] != '<':
|
|
251
|
+
array = array.byteswap(inplace=False)
|
|
252
|
+
data = array.tobytes(order='C')
|
|
253
|
+
n = len(data)
|
|
254
|
+
pad4 = -(off + n) & 3
|
|
255
|
+
off += pad4
|
|
256
|
+
pad_align = -(off + n) & (array.dtype.alignment - 1)
|
|
257
|
+
parts.append(pads[pad_align + pad4])
|
|
258
|
+
parts.append(data)
|
|
259
|
+
parts.append(uint32_pack(array.size))
|
|
260
|
+
off += pad_align + n + 4
|
|
261
|
+
else: # _BYTES, _INT8_LIST, _FLEX
|
|
262
|
+
if kind == _INT8_LIST:
|
|
263
|
+
data = py_array('b', val).tobytes()
|
|
264
|
+
elif kind == _FLEX:
|
|
265
|
+
flex_builder = self._flex_builder()
|
|
266
|
+
flex_builder.Add(val)
|
|
267
|
+
data = bytes(flex_builder.Finish())
|
|
268
|
+
elif not isinstance(val, bytes):
|
|
269
|
+
raise TypeError("non-byte vector passed to CreateByteVector")
|
|
270
|
+
else:
|
|
271
|
+
data = val
|
|
272
|
+
n = len(data)
|
|
273
|
+
pad = -(off + n) & 3
|
|
274
|
+
parts.append(pads[pad])
|
|
275
|
+
parts.append(data)
|
|
276
|
+
parts.append(uint32_pack(n))
|
|
277
|
+
off += pad + n + 4
|
|
278
|
+
var_offs.append(off)
|
|
279
|
+
|
|
280
|
+
# Steps 2-4: the fields in memory order (reverse _properties order)
|
|
281
|
+
layout = self._layout(off & 7) if nulls is None else self._null_layout(off & 7, nulls)
|
|
282
|
+
offset_adjust = layout.offset_adjust
|
|
283
|
+
offset_field = 0
|
|
284
|
+
values = []
|
|
285
|
+
for kind, name, prop, data in self._fields_in_memory_order:
|
|
286
|
+
if kind == _FIELD_SCALAR:
|
|
287
|
+
val = getattr(obj, name)
|
|
288
|
+
if val is prop:
|
|
289
|
+
val = data
|
|
290
|
+
elif val is None:
|
|
291
|
+
if nulls is None:
|
|
292
|
+
return self.marshal_with_nulls(obj, id)
|
|
293
|
+
continue
|
|
294
|
+
values.append(val)
|
|
295
|
+
elif kind == _FIELD_OFFSET:
|
|
296
|
+
var_off = var_offs[data]
|
|
297
|
+
if var_off is None:
|
|
298
|
+
continue # null
|
|
299
|
+
values.append(off + offset_adjust[offset_field] - var_off)
|
|
300
|
+
offset_field += 1
|
|
301
|
+
elif kind == _FIELD_ID:
|
|
302
|
+
values.append(id)
|
|
303
|
+
elif kind == _FIELD_DATE:
|
|
304
|
+
val = getattr(obj, name)
|
|
305
|
+
if val is None:
|
|
306
|
+
if nulls is None:
|
|
307
|
+
return self.marshal_with_nulls(obj, id)
|
|
308
|
+
continue
|
|
309
|
+
values.append(date_value_to_int(0.0 if val is prop else val, data))
|
|
310
|
+
else: # _FIELD_BOOL
|
|
311
|
+
val = getattr(obj, name)
|
|
312
|
+
if val is prop:
|
|
313
|
+
val = False
|
|
314
|
+
elif val is None:
|
|
315
|
+
if nulls is None:
|
|
316
|
+
return self.marshal_with_nulls(obj, id)
|
|
317
|
+
continue
|
|
318
|
+
elif not (False <= val <= True):
|
|
319
|
+
raise TypeError("bad number %s for type bool" % str(val)) # as flatbuffers' enforce_number
|
|
320
|
+
values.append(val)
|
|
321
|
+
parts.append(layout.fields.pack(*values))
|
|
322
|
+
parts.append(layout.prefix)
|
|
323
|
+
parts.reverse()
|
|
324
|
+
return b''.join(parts)
|