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