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.
@@ -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)