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,401 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import random
|
|
3
|
+
from typing import *
|
|
4
|
+
from objectbox.logger import logger
|
|
5
|
+
from objectbox.model.model import Model
|
|
6
|
+
from objectbox.model.entity import _Entity, camel_case
|
|
7
|
+
from objectbox.model.properties import Property, Index, HnswIndex, Relation
|
|
8
|
+
from objectbox.model.iduid import IdUid
|
|
9
|
+
|
|
10
|
+
MODEL_PARSER_VERSION = 5
|
|
11
|
+
MODEL_VERSION = 5 # Of the model JSON format; like the other ObjectBox languages (e.g. objectbox-generator checks it)
|
|
12
|
+
|
|
13
|
+
# Lists of the UIDs of removed entities, properties, indexes and relations, like in the model JSON of the other
|
|
14
|
+
# ObjectBox languages: they must not be used again, and e.g. the objectbox-generator (Go, C++) checks "last" IDs
|
|
15
|
+
# against them.
|
|
16
|
+
_RETIRED_KEYS = ("retiredEntityUids", "retiredIndexUids", "retiredPropertyUids", "retiredRelationUids")
|
|
17
|
+
|
|
18
|
+
# Top-level fields of the model JSON that Python does not manage, e.g. written by another ObjectBox language for a
|
|
19
|
+
# shared file; kept as they are when the file is written
|
|
20
|
+
_KEPT_KEYS = ("lastRelationId", "lastSequenceId", "modelVersion", "version")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class IdSync:
|
|
24
|
+
"""
|
|
25
|
+
Synchronizes a model with the IDs from model JSON file.
|
|
26
|
+
After syncing, the model will have all IDs assigned.
|
|
27
|
+
The JSON file is written (from scratch) based on the model.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, model: Model, model_json_filepath: str):
|
|
31
|
+
self.model = model
|
|
32
|
+
if len(model.entities) == 0:
|
|
33
|
+
raise ValueError("A valid model must have at least one entity")
|
|
34
|
+
|
|
35
|
+
self.model_filepath = model_json_filepath
|
|
36
|
+
self.model_json = None
|
|
37
|
+
|
|
38
|
+
self._assigned_uids: Set[int] = set()
|
|
39
|
+
self._retired_uids: Set[int] = set()
|
|
40
|
+
self._retired: Dict[str, List[int]] = {key: [] for key in _RETIRED_KEYS} # In order, as written to the JSON
|
|
41
|
+
self._matched_uids: Set[int] = set() # Of JSON entities, properties and indexes matched with the model
|
|
42
|
+
self._entity_json_by_uid: Dict[int, Dict[str, Any]] = {} # Matched JSON entity by its UID
|
|
43
|
+
# How an unassigned "last" ID is written: "0:0" (like Java), or "" if the loaded JSON does so (like the
|
|
44
|
+
# objectbox-generator for Go and C++, which rejects "0:0")
|
|
45
|
+
self._unassigned_str = "0:0"
|
|
46
|
+
|
|
47
|
+
self._load_model_json()
|
|
48
|
+
|
|
49
|
+
def _load_model_json(self):
|
|
50
|
+
import json
|
|
51
|
+
from os import path
|
|
52
|
+
|
|
53
|
+
if not path.exists(self.model_filepath):
|
|
54
|
+
logger.debug(f"Model file not found: {self.model_filepath}")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
with open(self.model_filepath, "rt") as model_file:
|
|
58
|
+
self.model_json = json.load(model_file)
|
|
59
|
+
logger.debug(f"Syncing model with model file: {self.model_filepath}")
|
|
60
|
+
if any(self.model_json.get(key) == "" for key in ("lastEntityId", "lastIndexId", "lastRelationId")):
|
|
61
|
+
self._unassigned_str = ""
|
|
62
|
+
|
|
63
|
+
self._load_assigned_uids()
|
|
64
|
+
|
|
65
|
+
def _load_assigned_uids(self):
|
|
66
|
+
for key in _RETIRED_KEYS:
|
|
67
|
+
for uid in self.model_json.get(key) or []:
|
|
68
|
+
uid = int(uid)
|
|
69
|
+
if uid not in self._retired_uids:
|
|
70
|
+
self._retired[key].append(uid)
|
|
71
|
+
self._retired_uids.add(uid)
|
|
72
|
+
self._assigned_uids |= self._retired_uids
|
|
73
|
+
|
|
74
|
+
for entity_json in self.model_json["entities"]:
|
|
75
|
+
entity_uid = IdUid.from_str(entity_json["id"]).uid
|
|
76
|
+
if entity_uid in self._assigned_uids:
|
|
77
|
+
raise ValueError(f"An entity's UID {entity_uid} has already been used elsewhere")
|
|
78
|
+
self._assigned_uids.add(entity_uid)
|
|
79
|
+
|
|
80
|
+
for prop_json in entity_json["properties"]:
|
|
81
|
+
prop_uid = IdUid.from_str(prop_json["id"]).uid
|
|
82
|
+
if prop_uid in self._assigned_uids:
|
|
83
|
+
raise ValueError(f"A property's UID {prop_uid} has already been used elsewhere")
|
|
84
|
+
self._assigned_uids.add(prop_uid)
|
|
85
|
+
|
|
86
|
+
if "indexId" in prop_json:
|
|
87
|
+
index_uid = IdUid.from_str(prop_json["indexId"]).uid
|
|
88
|
+
if index_uid in self._assigned_uids:
|
|
89
|
+
raise ValueError(f"An index's UID {index_uid} has already been used elsewhere")
|
|
90
|
+
self._assigned_uids.add(index_uid)
|
|
91
|
+
|
|
92
|
+
# Standalone relations (e.g. ToMany in Java, Dart) are not supported in Python, but kept in the JSON
|
|
93
|
+
for relation_json in entity_json.get("relations") or []:
|
|
94
|
+
self._assigned_uids.add(IdUid.from_str(relation_json["id"]).uid)
|
|
95
|
+
|
|
96
|
+
def _iduid_str(self, iduid: IdUid) -> str:
|
|
97
|
+
return str(iduid) if iduid.is_assigned() else self._unassigned_str
|
|
98
|
+
|
|
99
|
+
def _save_model_json(self):
|
|
100
|
+
""" Replaces model JSON with the serialized model whose ID/UIDs are assigned. """
|
|
101
|
+
|
|
102
|
+
# model.validate_ids_assigned()
|
|
103
|
+
|
|
104
|
+
model_json = {
|
|
105
|
+
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
|
|
106
|
+
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
|
|
107
|
+
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
|
|
108
|
+
"modelVersionParserMinimum": MODEL_PARSER_VERSION,
|
|
109
|
+
"entities": [],
|
|
110
|
+
"lastEntityId": self._iduid_str(self.model.last_entity_iduid),
|
|
111
|
+
"lastIndexId": self._iduid_str(self.model.last_index_iduid),
|
|
112
|
+
"lastRelationId": self._iduid_str(self.model.last_relation_iduid),
|
|
113
|
+
"modelVersion": MODEL_VERSION,
|
|
114
|
+
"version": 1
|
|
115
|
+
}
|
|
116
|
+
if self.model_json is not None:
|
|
117
|
+
for key in _KEPT_KEYS:
|
|
118
|
+
if key in self.model_json:
|
|
119
|
+
model_json[key] = self.model_json[key]
|
|
120
|
+
model_json.update(self._retired)
|
|
121
|
+
|
|
122
|
+
for entity in self.model.entities:
|
|
123
|
+
entity_json = {
|
|
124
|
+
"id": str(entity._iduid),
|
|
125
|
+
"name": entity._name,
|
|
126
|
+
"lastPropertyId": str(entity._last_property_iduid),
|
|
127
|
+
"properties": []
|
|
128
|
+
}
|
|
129
|
+
if entity._flags != 0:
|
|
130
|
+
entity_json["flags"] = entity._flags # E.g. Sync enabled; like the model JSON of other ObjectBox APIs
|
|
131
|
+
for prop in entity._properties:
|
|
132
|
+
prop_json = {
|
|
133
|
+
"id": str(prop.iduid),
|
|
134
|
+
"name": prop._db_name,
|
|
135
|
+
"type": prop._ob_type,
|
|
136
|
+
}
|
|
137
|
+
if prop._flags != 0:
|
|
138
|
+
prop_json["flags"] = prop._flags
|
|
139
|
+
if prop.index is not None:
|
|
140
|
+
prop_json["indexId"] = str(prop.index.iduid)
|
|
141
|
+
if isinstance(prop, Relation):
|
|
142
|
+
prop_json["relationTarget"] = prop._target_name
|
|
143
|
+
entity_json["properties"].append(prop_json)
|
|
144
|
+
loaded_entity_json = self._entity_json_by_uid.get(entity._uid)
|
|
145
|
+
if loaded_entity_json is not None and "relations" in loaded_entity_json:
|
|
146
|
+
entity_json["relations"] = loaded_entity_json["relations"] # Kept, see _load_assigned_uids()
|
|
147
|
+
model_json["entities"].append(entity_json)
|
|
148
|
+
|
|
149
|
+
import json
|
|
150
|
+
with open(self.model_filepath, "w") as model_file:
|
|
151
|
+
model_file.write(json.dumps(model_json, indent=2)) # Pretty
|
|
152
|
+
|
|
153
|
+
# *** Sync ***
|
|
154
|
+
|
|
155
|
+
def _find_entity_json_by_uid(self, uid: int) -> Optional[Dict[str, Any]]:
|
|
156
|
+
""" Finds entity JSON by UID. """
|
|
157
|
+
if self.model_json is None:
|
|
158
|
+
return None
|
|
159
|
+
# TODO put entities in a dict (e.g. while/after loading) for faster lookup
|
|
160
|
+
for entity_json in self.model_json["entities"]:
|
|
161
|
+
if IdUid.from_str(entity_json["id"]).uid == uid:
|
|
162
|
+
return entity_json
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
def _find_entity_json_by_name(self, entity_name: str) -> Optional[Dict[str, Any]]:
|
|
166
|
+
""" Finds entity JSON by name. """
|
|
167
|
+
if self.model_json is None:
|
|
168
|
+
return None
|
|
169
|
+
# TODO put entities in a dict (e.g. while/after loading) for faster lookup
|
|
170
|
+
for entity_json in self.model_json["entities"]:
|
|
171
|
+
if entity_json["name"] == entity_name:
|
|
172
|
+
return entity_json
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
def _find_property_json_by_uid(self, entity_json: Dict[str, Any], uid: int) -> Optional[Dict[str, Any]]:
|
|
176
|
+
""" Finds entity property JSON by property UID. """
|
|
177
|
+
# TODO put properties in a multi-dict (e.g. while/after loading) for faster lookup
|
|
178
|
+
for prop_json in entity_json["properties"]:
|
|
179
|
+
if IdUid.from_str(prop_json["id"]).uid == uid:
|
|
180
|
+
return prop_json
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
def _find_property_json_by_name(self, entity_json: Dict[str, Any], prop_name: str) -> Optional[Dict[str, Any]]:
|
|
184
|
+
""" Finds entity property JSON by property name. """
|
|
185
|
+
# TODO put properties in a multi-dict (e.g. while/after loading) for faster lookup
|
|
186
|
+
for prop_json in entity_json["properties"]:
|
|
187
|
+
if prop_json["name"] == prop_name:
|
|
188
|
+
return prop_json
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
def _generate_uid(self) -> int:
|
|
192
|
+
while True:
|
|
193
|
+
generated_uid = random.getrandbits(63) + 1 # 0 would be invalid
|
|
194
|
+
if generated_uid not in self._assigned_uids:
|
|
195
|
+
break
|
|
196
|
+
self._assigned_uids.add(generated_uid)
|
|
197
|
+
return generated_uid
|
|
198
|
+
|
|
199
|
+
def _validate_uid_unassigned(self, uid: int):
|
|
200
|
+
""" Validates that a user supplied UID is not assigned for any other entity/property/index.
|
|
201
|
+
Raises a ValueError if the UID is already assigned elsewhere.
|
|
202
|
+
"""
|
|
203
|
+
if uid in self._retired_uids:
|
|
204
|
+
raise ValueError(f"User supplied UID {uid} was used by a removed entity, property or index; "
|
|
205
|
+
f"UIDs must not be used again (see the retired UIDs in the model JSON file)")
|
|
206
|
+
if uid in self._assigned_uids:
|
|
207
|
+
raise ValueError(f"User supplied UID {uid} is already assigned elsewhere")
|
|
208
|
+
|
|
209
|
+
def _validate_matching_prop(self, entity: _Entity, prop: Property, prop_json: Dict[str, Any]):
|
|
210
|
+
""" Validates that the given property matches the JSON property. """
|
|
211
|
+
try:
|
|
212
|
+
# Don't check name equality as the property could be matched by UID (rename)
|
|
213
|
+
# if validate_name and prop.name != prop_json["name"]:
|
|
214
|
+
# raise ValueError(f"name {prop.name} != name {prop_json['name']} (in JSON)")
|
|
215
|
+
# Flags (e.g. the index type or an assignable ID) and the index may change: the database is updated
|
|
216
|
+
# when the store is opened, and the JSON file is rewritten.
|
|
217
|
+
if prop._ob_type != prop_json["type"]:
|
|
218
|
+
raise ValueError(f"OBX type {prop._ob_type} != OBX type {prop_json['type']} (in JSON)")
|
|
219
|
+
except ValueError as error:
|
|
220
|
+
raise ValueError(f"Property {entity._name}.{prop.name} mismatches property found in JSON file: {error}")
|
|
221
|
+
|
|
222
|
+
def _sync_index(self, entity: _Entity, prop: Property, prop_json: Optional[Dict[str, Any]]) -> bool:
|
|
223
|
+
assert prop.index is not None
|
|
224
|
+
index = prop.index
|
|
225
|
+
|
|
226
|
+
write_json = False
|
|
227
|
+
|
|
228
|
+
# Fetch index ID/UID from JSON file
|
|
229
|
+
iduid_json = None
|
|
230
|
+
if (prop_json is not None) and ("indexId" in prop_json):
|
|
231
|
+
iduid_json = IdUid.from_str(prop_json["indexId"])
|
|
232
|
+
|
|
233
|
+
# User provided a UID not matching index's, make sure it's not assigned elsewhere
|
|
234
|
+
if index.has_uid() and (iduid_json is not None) and (index.uid != iduid_json.uid):
|
|
235
|
+
self._validate_uid_unassigned(index.uid)
|
|
236
|
+
|
|
237
|
+
# Generate UID only if not supplied by the user, and index isn't found in JSON
|
|
238
|
+
if not index.has_uid() and iduid_json is None:
|
|
239
|
+
index.iduid.uid = self._generate_uid()
|
|
240
|
+
|
|
241
|
+
if (iduid_json is not None) and (not index.has_uid() or index.iduid.uid == iduid_json.uid): # Load
|
|
242
|
+
index.iduid = IdUid.from_str(prop_json["indexId"])
|
|
243
|
+
self._matched_uids.add(index.iduid.uid)
|
|
244
|
+
else: # Assign new ID to new index
|
|
245
|
+
index.iduid = IdUid(self.model.last_index_iduid.id + 1, index.uid)
|
|
246
|
+
self.model.last_index_iduid = index.iduid
|
|
247
|
+
write_json = True
|
|
248
|
+
|
|
249
|
+
return write_json
|
|
250
|
+
|
|
251
|
+
def _sync_property(self, entity: _Entity, prop: Property, entity_json: Optional[Dict[str, Any]]) -> bool:
|
|
252
|
+
write_json = False
|
|
253
|
+
|
|
254
|
+
prop_json = None
|
|
255
|
+
if prop.has_uid():
|
|
256
|
+
if entity_json is not None:
|
|
257
|
+
prop_json = self._find_property_json_by_uid(entity_json, prop.uid)
|
|
258
|
+
if prop_json is None:
|
|
259
|
+
# User provided a UID not matching any property (within the entity), make sure it's not assigned
|
|
260
|
+
# elsewhere
|
|
261
|
+
self._validate_uid_unassigned(prop.uid)
|
|
262
|
+
else:
|
|
263
|
+
write_json = prop._db_name != prop_json["name"] # If renaming we shall update the JSON
|
|
264
|
+
else:
|
|
265
|
+
if entity_json is not None:
|
|
266
|
+
prop_json = self._find_property_json_by_name(entity_json, prop._db_name)
|
|
267
|
+
if prop_json is None:
|
|
268
|
+
# The other form of the name, e.g. the Python name stored before 5.0 (no camelCase conversion);
|
|
269
|
+
# matching it renames the property, keeping its data
|
|
270
|
+
other_name = prop.name if prop._db_name != prop.name else camel_case(prop.name)
|
|
271
|
+
if other_name != prop._db_name:
|
|
272
|
+
prop_json = self._find_property_json_by_name(entity_json, other_name)
|
|
273
|
+
write_json = prop_json is not None
|
|
274
|
+
|
|
275
|
+
if prop_json is not None: # Load existing IDs from JSON
|
|
276
|
+
# Property was matched with a JSON property (either by UID or by name), make sure they're equal
|
|
277
|
+
self._validate_matching_prop(entity, prop, prop_json)
|
|
278
|
+
write_json |= prop._flags != prop_json.get("flags", 0)
|
|
279
|
+
write_json |= (prop.index is not None) != ("indexId" in prop_json)
|
|
280
|
+
prop.iduid = IdUid.from_str(prop_json["id"])
|
|
281
|
+
self._matched_uids.add(prop.iduid.uid)
|
|
282
|
+
else: # Assign new ID to new property
|
|
283
|
+
if not prop.has_uid():
|
|
284
|
+
prop.iduid.uid = self._generate_uid()
|
|
285
|
+
prop.iduid = IdUid(entity._last_property_iduid.id + 1, prop.iduid.uid)
|
|
286
|
+
entity._last_property_iduid = prop.iduid
|
|
287
|
+
write_json = True
|
|
288
|
+
|
|
289
|
+
if prop.index is not None:
|
|
290
|
+
write_json |= self._sync_index(entity, prop, prop_json)
|
|
291
|
+
|
|
292
|
+
return write_json
|
|
293
|
+
|
|
294
|
+
def _sync_entity(self, entity: _Entity) -> bool:
|
|
295
|
+
write_json = False
|
|
296
|
+
|
|
297
|
+
# entity_json = None
|
|
298
|
+
if entity._has_uid():
|
|
299
|
+
entity_json = self._find_entity_json_by_uid(entity._uid)
|
|
300
|
+
if entity_json is None:
|
|
301
|
+
# User provided a UID not matching any entity, make sure it's not assigned elsewhere
|
|
302
|
+
self._validate_uid_unassigned(entity._uid)
|
|
303
|
+
else:
|
|
304
|
+
write_json = entity._name != entity_json["name"] # If renaming we shall update the JSON
|
|
305
|
+
else:
|
|
306
|
+
entity_json = self._find_entity_json_by_name(entity._name)
|
|
307
|
+
|
|
308
|
+
# Write JSON if the number of properties differs (to handle removed property)
|
|
309
|
+
if entity_json is not None:
|
|
310
|
+
write_json |= len(entity._properties) != len(entity_json["properties"])
|
|
311
|
+
|
|
312
|
+
if entity_json is not None: # Load existing IDs from JSON
|
|
313
|
+
entity._iduid = IdUid.from_str(entity_json["id"])
|
|
314
|
+
self._matched_uids.add(entity._uid)
|
|
315
|
+
self._entity_json_by_uid[entity._uid] = entity_json
|
|
316
|
+
entity._last_property_iduid = IdUid.from_str(entity_json["lastPropertyId"])
|
|
317
|
+
else: # Assign new ID to new entity
|
|
318
|
+
if not entity._has_uid():
|
|
319
|
+
entity._iduid.uid = self._generate_uid()
|
|
320
|
+
entity._iduid = IdUid(self.model.last_entity_iduid.id + 1, entity._iduid.uid)
|
|
321
|
+
self.model.last_entity_iduid = entity._iduid
|
|
322
|
+
entity._last_property_iduid = IdUid(0, 0)
|
|
323
|
+
write_json = True
|
|
324
|
+
|
|
325
|
+
# Load properties
|
|
326
|
+
for prop in entity._properties:
|
|
327
|
+
write_json |= self._sync_property(entity, prop, entity_json)
|
|
328
|
+
|
|
329
|
+
return write_json
|
|
330
|
+
|
|
331
|
+
def _retire(self, key: str, uid: int) -> bool:
|
|
332
|
+
if uid == 0 or uid in self._retired_uids:
|
|
333
|
+
return False
|
|
334
|
+
self._retired[key].append(uid)
|
|
335
|
+
self._retired_uids.add(uid)
|
|
336
|
+
return True
|
|
337
|
+
|
|
338
|
+
def _retire_unmatched(self) -> bool:
|
|
339
|
+
""" Retires the UIDs of the JSON entities, properties and indexes not matched with the model (i.e. removed),
|
|
340
|
+
including all of a removed entity (also its relations), like the other ObjectBox languages.
|
|
341
|
+
Returns True if any UIDs were retired. """
|
|
342
|
+
if self.model_json is None:
|
|
343
|
+
return False
|
|
344
|
+
retired = False
|
|
345
|
+
for entity_json in self.model_json["entities"]:
|
|
346
|
+
entity_uid = IdUid.from_str(entity_json["id"]).uid
|
|
347
|
+
entity_removed = entity_uid not in self._matched_uids
|
|
348
|
+
if entity_removed:
|
|
349
|
+
retired |= self._retire("retiredEntityUids", entity_uid)
|
|
350
|
+
for relation_json in entity_json.get("relations") or []:
|
|
351
|
+
retired |= self._retire("retiredRelationUids", IdUid.from_str(relation_json["id"]).uid)
|
|
352
|
+
for prop_json in entity_json["properties"]:
|
|
353
|
+
prop_uid = IdUid.from_str(prop_json["id"]).uid
|
|
354
|
+
if entity_removed or prop_uid not in self._matched_uids:
|
|
355
|
+
retired |= self._retire("retiredPropertyUids", prop_uid)
|
|
356
|
+
if "indexId" in prop_json:
|
|
357
|
+
index_uid = IdUid.from_str(prop_json["indexId"]).uid
|
|
358
|
+
if entity_removed or index_uid not in self._matched_uids:
|
|
359
|
+
retired |= self._retire("retiredIndexUids", index_uid)
|
|
360
|
+
return retired
|
|
361
|
+
|
|
362
|
+
def sync(self) -> bool:
|
|
363
|
+
""" Syncs the provided model with the model JSON file.
|
|
364
|
+
Returns True if the model JSON was written. """
|
|
365
|
+
|
|
366
|
+
self.model._validate() # before writing the JSON
|
|
367
|
+
|
|
368
|
+
if self.model_json is not None:
|
|
369
|
+
self.model.last_entity_iduid = IdUid.from_str(self.model_json["lastEntityId"])
|
|
370
|
+
self.model.last_index_iduid = IdUid.from_str(self.model_json["lastIndexId"])
|
|
371
|
+
# self.model.last_relation_iduid =
|
|
372
|
+
|
|
373
|
+
write_json = False
|
|
374
|
+
|
|
375
|
+
# Write JSON if the number of entities differs (to handle removed entity)
|
|
376
|
+
if self.model_json is not None:
|
|
377
|
+
write_json |= len(self.model_json["entities"]) != len(self.model.entities)
|
|
378
|
+
|
|
379
|
+
for entity in self.model.entities:
|
|
380
|
+
write_json |= self._sync_entity(entity)
|
|
381
|
+
|
|
382
|
+
write_json |= self._retire_unmatched()
|
|
383
|
+
|
|
384
|
+
if write_json:
|
|
385
|
+
logger.info(f"Model changed, writing model.json: {self.model_filepath}")
|
|
386
|
+
self._save_model_json()
|
|
387
|
+
|
|
388
|
+
self.model.on_sync() # Notify model synced
|
|
389
|
+
|
|
390
|
+
return write_json
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def sync_model(model: Model, model_filepath: str = "objectbox-model.json") -> bool:
|
|
394
|
+
""" Syncs the provided model with the model JSON file.
|
|
395
|
+
Returns True if changes were made and the model JSON was written. """
|
|
396
|
+
|
|
397
|
+
id_sync = IdSync(model, model_filepath)
|
|
398
|
+
written = id_sync.sync()
|
|
399
|
+
model._synced_json_file = os.path.realpath(model_filepath)
|
|
400
|
+
model._synced_json_file_given = model_filepath
|
|
401
|
+
return written
|
objectbox/model/iduid.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
class IdUid:
|
|
2
|
+
__slots__ = 'id', 'uid'
|
|
3
|
+
|
|
4
|
+
def __init__(self, id_: int, uid: int):
|
|
5
|
+
self.id = id_
|
|
6
|
+
self.uid = uid
|
|
7
|
+
|
|
8
|
+
def is_assigned(self):
|
|
9
|
+
""" Checks that both ID and UID are assigned. Shall be true after the model is synced. """
|
|
10
|
+
return self.id != 0 and self.uid != 0
|
|
11
|
+
|
|
12
|
+
def __bool__(self):
|
|
13
|
+
return self.is_assigned()
|
|
14
|
+
|
|
15
|
+
def __eq__(self, rhs: 'IdUid'):
|
|
16
|
+
return self.id == rhs.id and self.uid == rhs.uid
|
|
17
|
+
|
|
18
|
+
def __str__(self):
|
|
19
|
+
return f"{self.id}:{self.uid}"
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def from_str(str_: str):
|
|
23
|
+
""" Parses IdUid from a string formatted like: "id:uid"; an empty string (e.g. "lastIndexId" written by
|
|
24
|
+
the C++ and Go generators if there is none) is unassigned, like "0:0". """
|
|
25
|
+
if str_ == "":
|
|
26
|
+
return IdUid.unassigned()
|
|
27
|
+
tmp = str_.split(":")
|
|
28
|
+
return IdUid(int(tmp[0]), int(tmp[1]))
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def unassigned():
|
|
32
|
+
return IdUid(0, 0)
|
objectbox/model/model.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Copyright 2019-2024 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
|
+
|
|
16
|
+
from objectbox.logger import logger
|
|
17
|
+
from objectbox.c import *
|
|
18
|
+
from objectbox.model.iduid import IdUid
|
|
19
|
+
from objectbox.model.entity import _Entity
|
|
20
|
+
from objectbox.model.properties import *
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Model:
|
|
24
|
+
"""
|
|
25
|
+
Database schema
|
|
26
|
+
|
|
27
|
+
A model specifies available entities in the database. Amongst others it uses this information to supports migration over time.
|
|
28
|
+
"""
|
|
29
|
+
def __init__(self):
|
|
30
|
+
self.entities: List[_Entity] = []
|
|
31
|
+
|
|
32
|
+
self.last_entity_iduid = IdUid(0, 0)
|
|
33
|
+
self.last_index_iduid = IdUid(0, 0)
|
|
34
|
+
self.last_relation_iduid = IdUid(0, 0)
|
|
35
|
+
|
|
36
|
+
self._c_model = None
|
|
37
|
+
|
|
38
|
+
# Model JSON file (real path) this model's IDs were synced with; None if not synced (yet).
|
|
39
|
+
# Used by Store to skip syncing again, e.g. when opening many stores with the same model.
|
|
40
|
+
self._synced_json_file: Optional[str] = None
|
|
41
|
+
self._synced_json_file_given: Optional[str] = None # ...as given (not resolved), for a quick comparison
|
|
42
|
+
|
|
43
|
+
def on_sync(self):
|
|
44
|
+
""" Method called once ID/UID are synced with the model file. """
|
|
45
|
+
for entity in self.entities:
|
|
46
|
+
entity._on_sync()
|
|
47
|
+
|
|
48
|
+
def entity(self, entity: _Entity):
|
|
49
|
+
if not isinstance(entity, _Entity):
|
|
50
|
+
raise TypeError(f"The given type is not an Entity: {type(entity)}. "
|
|
51
|
+
f"Ensure to have an @Entity annotation on the class.")
|
|
52
|
+
for other_entity in self.entities: # Linear search (we should't have many entities)
|
|
53
|
+
if entity._name == other_entity._name:
|
|
54
|
+
raise ValueError(f"Duplicate entity: \"{entity._name}\"")
|
|
55
|
+
self.entities.append(entity)
|
|
56
|
+
self._synced_json_file = None # Changed model; must be synced again
|
|
57
|
+
self._synced_json_file_given = None
|
|
58
|
+
|
|
59
|
+
def validate_ids_assigned(self):
|
|
60
|
+
# TODO validate last_relation_iduid
|
|
61
|
+
has_entities = len(self.entities) > 0
|
|
62
|
+
has_indices = False
|
|
63
|
+
for entity in self.entities:
|
|
64
|
+
has_properties = len(entity._properties) > 0
|
|
65
|
+
if not entity._iduid.is_assigned():
|
|
66
|
+
raise ValueError(f"Entity \"{entity._name}\" ID/UID not assigned")
|
|
67
|
+
for prop in entity._properties:
|
|
68
|
+
if not prop.iduid.is_assigned():
|
|
69
|
+
raise ValueError(f"Property \"{entity._name}.{prop.name}\" ID/UID not assigned")
|
|
70
|
+
if prop.index is not None:
|
|
71
|
+
has_indices = True
|
|
72
|
+
if not prop.index.iduid.is_assigned():
|
|
73
|
+
raise ValueError(f"Property index \"{entity._name}.{prop.name}\" ID/UID not assigned")
|
|
74
|
+
if has_properties and not entity._last_property_iduid.is_assigned():
|
|
75
|
+
raise ValueError(f"Entity \"{entity._name}\" last property ID/UID not assigned")
|
|
76
|
+
if has_entities and not self.last_entity_iduid.is_assigned():
|
|
77
|
+
raise ValueError("Last entity ID/UID not assigned")
|
|
78
|
+
if has_indices and not self.last_index_iduid.is_assigned():
|
|
79
|
+
raise ValueError("Last index ID/UID not assigned")
|
|
80
|
+
|
|
81
|
+
def _validate(self):
|
|
82
|
+
""" Validates what can only be checked with all entities (and their flags) defined. """
|
|
83
|
+
self._validate_relation_targets()
|
|
84
|
+
self._validate_unique()
|
|
85
|
+
|
|
86
|
+
def _validate_unique(self):
|
|
87
|
+
for entity in self.entities:
|
|
88
|
+
unique = [prop for prop in entity._properties if prop.unique is not None]
|
|
89
|
+
replace = [prop.name for prop in unique if prop.unique.on_conflict == ConflictStrategy.REPLACE]
|
|
90
|
+
if len(replace) > 1:
|
|
91
|
+
raise ValueError(f"Entity {entity._name}: ConflictStrategy.REPLACE can only be used for one property, "
|
|
92
|
+
f"but is used for {', '.join(replace)}")
|
|
93
|
+
if entity._flags & OBXEntityFlags.SYNC_ENABLED:
|
|
94
|
+
not_replace = [prop.name for prop in unique if prop.unique.on_conflict != ConflictStrategy.REPLACE]
|
|
95
|
+
if not_replace:
|
|
96
|
+
raise ValueError(f"Sync entity {entity._name}: unique properties must use "
|
|
97
|
+
f"Unique(ConflictStrategy.REPLACE), but {', '.join(not_replace)} do(es) not")
|
|
98
|
+
|
|
99
|
+
def _validate_relation_targets(self):
|
|
100
|
+
names = {entity._name for entity in self.entities}
|
|
101
|
+
for entity in self.entities:
|
|
102
|
+
for prop in entity._properties:
|
|
103
|
+
if isinstance(prop, Relation) and prop._target_name not in names:
|
|
104
|
+
raise ValueError(f"Relation {entity._name}.{prop.name} targets entity \"{prop._target_name}\", "
|
|
105
|
+
f"which is not in the model")
|
|
106
|
+
|
|
107
|
+
def _set_hnsw_params(self, index: HnswIndex):
|
|
108
|
+
if index.dimensions is not None:
|
|
109
|
+
obx_model_property_index_hnsw_dimensions(self._c_model, index.dimensions)
|
|
110
|
+
if index.neighbors_per_node is not None:
|
|
111
|
+
obx_model_property_index_hnsw_neighbors_per_node(self._c_model, index.neighbors_per_node)
|
|
112
|
+
if index.indexing_search_count is not None:
|
|
113
|
+
obx_model_property_index_hnsw_indexing_search_count(self._c_model, index.indexing_search_count)
|
|
114
|
+
if index.flags is not None:
|
|
115
|
+
obx_model_property_index_hnsw_flags(self._c_model, index.flags)
|
|
116
|
+
if index.distance_type is not None:
|
|
117
|
+
obx_model_property_index_hnsw_distance_type(self._c_model, index.distance_type)
|
|
118
|
+
if index.reparation_backlink_probability is not None:
|
|
119
|
+
obx_model_property_index_hnsw_reparation_backlink_probability(self._c_model,
|
|
120
|
+
index.reparation_backlink_probability)
|
|
121
|
+
if index.vector_cache_hint_size_kb is not None:
|
|
122
|
+
obx_model_property_index_hnsw_vector_cache_hint_size_kb(self._c_model, index.vector_cache_hint_size_kb)
|
|
123
|
+
|
|
124
|
+
def _create_index(self, index: Union[Index, HnswIndex]):
|
|
125
|
+
if isinstance(index, HnswIndex):
|
|
126
|
+
self._set_hnsw_params(index)
|
|
127
|
+
obx_model_property_index_id(self._c_model, index.id, index.uid)
|
|
128
|
+
|
|
129
|
+
def _create_property(self, prop: Property):
|
|
130
|
+
obx_model_property(self._c_model, c_str(prop._db_name), prop._ob_type, prop.id, prop.uid)
|
|
131
|
+
if prop._flags != 0:
|
|
132
|
+
obx_model_property_flags(self._c_model, prop._flags)
|
|
133
|
+
if isinstance(prop, Relation):
|
|
134
|
+
obx_model_property_relation(self._c_model, c_str(prop._target_name), prop.index.id, prop.index.uid)
|
|
135
|
+
elif prop.index is not None:
|
|
136
|
+
self._create_index(prop.index)
|
|
137
|
+
|
|
138
|
+
def _create_entity(self, entity: _Entity):
|
|
139
|
+
obx_model_entity(self._c_model, c_str(entity._name), entity._id, entity._uid)
|
|
140
|
+
obx_model_entity_flags(self._c_model, entity._flags)
|
|
141
|
+
for prop in entity._properties:
|
|
142
|
+
self._create_property(prop)
|
|
143
|
+
obx_model_entity_last_property_id(self._c_model, entity._last_property_iduid.id, entity._last_property_iduid.uid)
|
|
144
|
+
|
|
145
|
+
def _create_c_model(self) -> obx_model: # Called by StoreOptions
|
|
146
|
+
""" Creates the OBX model by invoking the C API.
|
|
147
|
+
Before calling this method, IDs/UIDs must be assigned either manually or via sync_model(). """
|
|
148
|
+
self._validate()
|
|
149
|
+
self._c_model = obx_model()
|
|
150
|
+
for entity in self.entities:
|
|
151
|
+
self._create_entity(entity)
|
|
152
|
+
if self.last_relation_iduid:
|
|
153
|
+
obx_model_last_relation_id(self._c_model, self.last_relation_iduid.id, self.last_relation_iduid.uid)
|
|
154
|
+
if self.last_index_iduid:
|
|
155
|
+
obx_model_last_index_id(self._c_model, self.last_index_iduid.id, self.last_index_iduid.uid)
|
|
156
|
+
if self.last_entity_iduid:
|
|
157
|
+
obx_model_last_entity_id(self._c_model, self.last_entity_iduid.id, self.last_entity_iduid.uid)
|
|
158
|
+
return self._c_model
|