Skip to content

Commit 82bbb34

Browse files
committed
Move sync_model to IdSync, handle user supplied UIDs #25
Use case: entity/property/... renaming
1 parent 3cbc99c commit 82bbb34

10 files changed

Lines changed: 390 additions & 211 deletions

File tree

objectbox/box.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def __init__(self, store: Store, entity: _Entity):
2727

2828
self._store = store
2929
self._entity = entity
30-
self._c_box = obx_box(store._c_store, entity.id.id)
30+
self._c_box = obx_box(store._c_store, entity.id)
3131

3232
def is_empty(self) -> bool:
3333
is_empty = ctypes.c_bool()

objectbox/model/entity.py

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,29 @@
3030

3131
# _Entity class holds model information as well as conversions between python objects and FlatBuffers (ObjectBox data)
3232
class _Entity(object):
33-
def __init__(self, user_type, id_: IdUid):
33+
def __init__(self, user_type, uid: int = 0):
3434
self.user_type = user_type
35-
self.id = id_
35+
self.iduid = IdUid(0, uid)
3636
self.name = user_type.__name__
37+
self.last_property_iduid = IdUid(0, 0)
3738

38-
self.last_property_id = IdUid.unassigned()
39-
40-
self.properties = list() # List[Property]
39+
self.properties: List[Property] = list() # List[Property]
4140
self.offset_properties = list() # List[Property]
4241
self.id_property = None
4342
self.fill_properties()
4443
self._tl = threading.local()
4544

45+
@property
46+
def id(self) -> int:
47+
return self.iduid.id
48+
49+
@property
50+
def uid(self) -> int:
51+
return self.iduid.uid
52+
53+
def has_uid(self) -> bool:
54+
return self.iduid.uid != 0
55+
4656
def __call__(self, **properties):
4757
""" The constructor of the user Entity class. """
4858
object_ = self.user_type()
@@ -103,9 +113,9 @@ def get_property_id(self, prop: Union[int, str, Property]) -> int:
103113
if isinstance(prop, int):
104114
return prop # We already have it!
105115
elif isinstance(prop, str):
106-
return self.get_property(prop).id.id
116+
return self.get_property(prop).id
107117
elif isinstance(prop, Property):
108-
return prop.id.id
118+
return prop.id
109119
else:
110120
raise Exception(f"Unsupported Property type: {type(prop)}")
111121

@@ -141,42 +151,41 @@ def marshal(self, object, id: int) -> bytearray:
141151
# prepare some properties that need to be built in FB before starting the main object
142152
offsets = {}
143153
for prop in self.offset_properties:
144-
prop_id = prop.id.id
145154
val = self.get_value(object, prop)
146155
if prop._ob_type == OBXPropertyType_String:
147-
offsets[prop_id] = builder.CreateString(val.encode('utf-8'))
156+
offsets[prop.id] = builder.CreateString(val.encode('utf-8'))
148157
elif prop._ob_type == OBXPropertyType_BoolVector:
149158
# Using a numpy bool as it seems to be more consistent in terms of size. TBD
150159
# https://numpy.org/doc/stable/reference/arrays.scalars.html#numpy.bool
151-
offsets[prop_id] = builder.CreateNumpyVector(np.array(val, dtype=np.bool_))
160+
offsets[prop.id] = builder.CreateNumpyVector(np.array(val, dtype=np.bool_))
152161
elif prop._ob_type == OBXPropertyType_ByteVector:
153-
offsets[prop_id] = builder.CreateByteVector(val)
162+
offsets[prop.id] = builder.CreateByteVector(val)
154163
elif prop._ob_type == OBXPropertyType_ShortVector:
155-
offsets[prop_id] = builder.CreateNumpyVector(np.array(val, dtype=np.int16))
164+
offsets[prop.id] = builder.CreateNumpyVector(np.array(val, dtype=np.int16))
156165
elif prop._ob_type == OBXPropertyType_CharVector:
157-
offsets[prop_id] = builder.CreateNumpyVector(np.array(val, dtype=np.uint16))
166+
offsets[prop.id] = builder.CreateNumpyVector(np.array(val, dtype=np.uint16))
158167
elif prop._ob_type == OBXPropertyType_IntVector:
159-
offsets[prop_id] = builder.CreateNumpyVector(np.array(val, dtype=np.int32))
168+
offsets[prop.id] = builder.CreateNumpyVector(np.array(val, dtype=np.int32))
160169
elif prop._ob_type == OBXPropertyType_LongVector:
161-
offsets[prop_id] = builder.CreateNumpyVector(np.array(val, dtype=np.int64))
170+
offsets[prop.id] = builder.CreateNumpyVector(np.array(val, dtype=np.int64))
162171
elif prop._ob_type == OBXPropertyType_FloatVector:
163-
offsets[prop_id] = builder.CreateNumpyVector(np.array(val, dtype=np.float32))
172+
offsets[prop.id] = builder.CreateNumpyVector(np.array(val, dtype=np.float32))
164173
elif prop._ob_type == OBXPropertyType_DoubleVector:
165-
offsets[prop_id] = builder.CreateNumpyVector(np.array(val, dtype=np.float64))
174+
offsets[prop.id] = builder.CreateNumpyVector(np.array(val, dtype=np.float64))
166175
elif prop._ob_type == OBXPropertyType_Flex:
167176
flex_builder = flatbuffers.flexbuffers.Builder()
168177
flex_builder.Add(val)
169178
buffer = flex_builder.Finish()
170-
offsets[prop_id] = builder.CreateByteVector(bytes(buffer))
179+
offsets[prop.id] = builder.CreateByteVector(bytes(buffer))
171180
else:
172181
assert False, "programming error - invalid type OB & FB type combination"
173182

174183
# start the FlatBuffers object with the largest number of properties that were ever present in the Entity
175-
builder.StartObject(self.last_property_id.id)
184+
builder.StartObject(self.last_property_iduid.id)
176185

177186
# add properties to the FB object
178187
for prop in self.properties:
179-
prop_id = prop.id.id
188+
prop_id = prop.id
180189
if prop_id in offsets:
181190
val = offsets[prop_id]
182191
if val:
@@ -207,8 +216,7 @@ def unmarshal(self, data: bytes):
207216

208217
# fill it with the data read from FlatBuffers
209218
for prop in self.properties:
210-
prop_id = prop.id.id
211-
fb_slot = prop_id - 1
219+
fb_slot = prop.id - 1
212220
fb_v_offset = 4 + 2 * fb_slot
213221
# 4 + 2 * fb_slot
214222
o = table.Offset(fb_v_offset)
@@ -265,10 +273,10 @@ def unmarshal(self, data: bytes):
265273
return obj
266274

267275

268-
def Entity(id_: IdUid = IdUid.unassigned()) -> Callable[[Type], _Entity]:
276+
def Entity(uid: int = 0) -> Callable[[Type], _Entity]:
269277
""" Entity decorator that wraps _Entity to allow @Entity(id=, uid=); i.e. no class arguments. """
270278

271279
def wrapper(class_):
272-
return _Entity(class_, id_)
280+
return _Entity(class_, uid)
273281

274282
return wrapper

objectbox/model/idsync.py

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import random
2+
from typing import *
3+
from objectbox.logger import logger
4+
from objectbox.model import Model
5+
from objectbox.model.entity import _Entity
6+
from objectbox.model.properties import Property, Index, HnswIndex
7+
from objectbox.model.iduid import IdUid
8+
9+
MODEL_PARSER_VERSION = 5
10+
11+
12+
class IdSync:
13+
def __init__(self, model: Model, model_json_filepath: str):
14+
self.model = model
15+
16+
self.model_filepath = model_json_filepath
17+
self.model_json = None
18+
self._load_model_json()
19+
20+
def _load_model_json(self):
21+
import json
22+
from os import path
23+
24+
if not path.exists(self.model_filepath):
25+
logger.debug(f"Model file not found: {self.model_filepath}")
26+
return
27+
28+
with open(self.model_filepath, "rt") as model_file:
29+
self.model_json = json.load(model_file)
30+
logger.debug(f"Syncing model with model file: {self.model_filepath}")
31+
32+
def _save_model_json(self):
33+
""" Replaces model JSON with the serialized model whose ID/UIDs are assigned. """
34+
35+
# model.validate_ids_assigned()
36+
37+
model_json = {
38+
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
39+
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
40+
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
41+
"modelVersionParserMinimum": MODEL_PARSER_VERSION,
42+
"entities": [],
43+
"lastEntityId": str(self.model.last_entity_iduid),
44+
"lastIndexId": str(self.model.last_index_iduid)
45+
}
46+
# TODO lastRelationId
47+
# TODO modelVersion
48+
# TODO retiredEntityUids
49+
# TODO retiredIndexUids
50+
# TODO retiredPropertyUids
51+
# TODO retiredRelationUids
52+
# TODO version
53+
54+
for entity in self.model.entities:
55+
entity_json = {
56+
"id": str(entity.iduid),
57+
"name": entity.name,
58+
"lastPropertyId": str(entity.last_property_iduid),
59+
"properties": []
60+
}
61+
for prop in entity.properties:
62+
prop_json = {
63+
"id": str(prop.iduid),
64+
"name": prop.name,
65+
"type": prop._ob_type,
66+
"flags": prop._flags
67+
}
68+
if prop.index is not None:
69+
prop_json["indexId"] = str(prop.index.iduid)
70+
entity_json["properties"].append(prop_json)
71+
model_json["entities"].append(entity_json)
72+
73+
import json
74+
with open(self.model_filepath, "w") as model_file:
75+
model_file.write(json.dumps(model_json, indent=2)) # Pretty
76+
77+
# *** Sync ***
78+
79+
def _find_entity_json_by_name(self, entity_name: str) -> Optional[Dict[str, Any]]:
80+
""" Finds the entity data by name in the model JSON file. """
81+
if self.model_json is None:
82+
return None
83+
for entity_json in self.model_json["entities"]:
84+
if entity_json["name"] == entity_name:
85+
return entity_json
86+
return None
87+
88+
def _find_property_json_by_name(self, entity_name: str, prop_name: str) -> Optional[Dict[str, Any]]:
89+
""" Finds the entity property data by name in the model JSON file. """
90+
entity_json = self._find_entity_json_by_name(entity_name)
91+
if entity_json is None:
92+
return None
93+
for prop_json in entity_json["properties"]:
94+
if prop_json["name"] == prop_name:
95+
return prop_json
96+
return None
97+
98+
@staticmethod
99+
def _generate_uid() -> int:
100+
return random.getrandbits(63) + 1 # 0 would be invalid
101+
102+
def _validate_uid_unassigned(self, uid: int):
103+
""" Validates that the UID is not assigned for any other entity/property/index. """
104+
pass # TODO
105+
106+
def _sync_index_id(self, entity: _Entity, prop: Property, index: Union[Index, HnswIndex]) -> None:
107+
""" Given an index, syncs its ID/UID with the JSON file. """
108+
iduid_json = None
109+
prop_json = self._find_property_json_by_name(entity.name, prop.name)
110+
if prop_json is not None and "indexId" in prop_json:
111+
iduid_json = IdUid.from_str(prop_json["indexId"])
112+
if iduid_json is None: # Index not present in JSON
113+
if index.has_uid():
114+
self._validate_uid_unassigned(index.uid)
115+
else:
116+
gen_uid = self._generate_uid()
117+
index.iduid.uid = gen_uid
118+
index.iduid = IdUid(self.model.last_index_iduid.id + 1, index.uid)
119+
else: # Index present in JSON
120+
if index.has_uid() and index.uid != iduid_json.uid:
121+
self._validate_uid_unassigned(index.uid)
122+
index.iduid = IdUid(self.model.last_index_iduid.id + 1, index.uid) # Assign ID
123+
else: # not index.has_uid() or index.uid != iduid_json.uid
124+
index.iduid = iduid_json
125+
126+
def _validate_matching_prop(self, entity: _Entity, prop: Property, prop_json: Dict[str, Any]):
127+
""" Validates that the given property matches the JSON property. """
128+
assert prop.name == prop_json["name"], \
129+
f"Property {entity.name}.{prop.name} mismatches property found in JSON file " \
130+
f"(name {prop.name} != type {prop_json['name']})" # Shouldn't happen (JSON property is got by name)
131+
assert prop._ob_type == prop_json["type"], \
132+
f"Property {entity.name}.{prop.name} mismatches property found in JSON file " \
133+
f"(type {prop._ob_type} != type {prop_json['type']})"
134+
assert prop._flags == prop_json["flags"], \
135+
f"Property {entity.name}.{prop.name} mismatches property found in JSON file " \
136+
f"(flags {prop._flags} != type {prop_json['flags']})"
137+
138+
def _sync_property_id(self, entity: _Entity, prop: Property) -> None:
139+
""" Given an entity's property, syncs its ID/UID with the JSON file. """
140+
prop_json = self._find_property_json_by_name(entity.name, prop.name)
141+
if prop_json is None: # Property not present in JSON
142+
if prop.has_uid():
143+
self._validate_uid_unassigned(prop.uid)
144+
else:
145+
prop.iduid.uid = self._generate_uid()
146+
prop.iduid = IdUid(entity.last_property_iduid.id + 1, prop.uid) # Assign ID
147+
else: # Property present in JSON
148+
iduid_json = IdUid.from_str(prop_json["id"])
149+
if prop.has_uid() and prop.uid != iduid_json.uid: # New property
150+
self._validate_uid_unassigned(prop.uid)
151+
prop.iduid = IdUid(entity.last_property_iduid.id + 1, prop.uid) # Assign ID
152+
else: # not prop.has_uid() or prop.uid == iduid_json.uid
153+
self._validate_matching_prop(entity, prop, prop_json)
154+
prop.iduid = iduid_json
155+
156+
def _validate_matching_entity(self, entity: _Entity, entity_json: Dict[str, Any]):
157+
""" Validates that the given entity matches the JSON entity. """
158+
assert entity.name == entity_json["name"], \
159+
f"Entity {entity.name} mismatches property found in JSON file " \
160+
f"(name {entity.name} != type {entity_json['name']})" # Shouldn't happen (JSON entity is got by name)
161+
assert len(entity.properties) == len(entity_json["properties"]), \
162+
f"Entity {entity.name} mismatches entity found in JSON file " \
163+
f"({len(entity.properties)} properties != {len(entity_json['properties'])} properties)"
164+
# TODO check relations count
165+
pass # TODO check properties' fields?
166+
167+
def _sync_entity_id(self, entity: _Entity) -> None:
168+
""" Given an entity, syncs its ID/UID with the JSON file. """
169+
entity_json = self._find_entity_json_by_name(entity.name)
170+
if entity_json is None: # Entity not present in JSON file
171+
if entity.has_uid():
172+
self._validate_uid_unassigned(entity.uid)
173+
else:
174+
entity.iduid.uid = self._generate_uid()
175+
entity.iduid = IdUid(self.model.last_entity_iduid.id + 1, entity.uid) # Assign ID
176+
else: # Entity present in JSON file
177+
iduid_json = IdUid.from_str(entity_json["id"])
178+
if entity.has_uid() and entity.uid != iduid_json.uid: # New entity
179+
self._validate_uid_unassigned(entity.uid)
180+
entity.iduid = IdUid(self.model.last_entity_iduid.id + 1, entity.uid) # Assign ID
181+
else: # not entity.has_uid() or entity.uid == iduid_json.uid
182+
self._validate_matching_entity(entity, entity_json)
183+
entity.iduid = iduid_json
184+
185+
def sync(self):
186+
""" Syncs the provided model with the model JSON file. """
187+
188+
# Sync entities ID/UID
189+
if self.model_json is not None:
190+
self.model.last_entity_iduid = IdUid.from_str(self.model_json["lastEntityId"])
191+
else:
192+
self.model.last_entity_iduid = IdUid(0, 0)
193+
for entity in self.model.entities:
194+
self._sync_entity_id(entity)
195+
if entity.id > self.model.last_entity_iduid.id: # If assignment occurred, update last_entity_iduid
196+
self.model.last_entity_iduid = entity.iduid
197+
198+
# Sync properties ID/UID
199+
for entity in self.model.entities:
200+
entity_json = self._find_entity_json_by_name(entity.name)
201+
if entity_json is not None:
202+
entity.last_property_iduid = IdUid.from_str(entity_json["lastPropertyId"])
203+
else:
204+
entity.last_property_iduid = IdUid(0, 0)
205+
for prop in entity.properties:
206+
self._sync_property_id(entity, prop)
207+
if prop.id > entity.last_property_iduid.id: # If assignment occurred, update last_property_iduid
208+
entity.last_property_iduid = prop.iduid
209+
210+
# Sync indexes ID/UID
211+
if self.model_json is not None:
212+
self.model.last_index_iduid = IdUid.from_str(self.model_json["lastIndexId"])
213+
else:
214+
self.model.last_index_iduid = IdUid(0, 0)
215+
for entity in self.model.entities:
216+
for prop in entity.properties:
217+
if prop.index is not None:
218+
index = prop.index
219+
self._sync_index_id(entity, prop, index)
220+
if index.id > self.model.last_index_iduid.id: # If assignment occurred, update last_index_iduid
221+
self.model.last_index_iduid = index.iduid
222+
223+
# TODO Sync relations ID/UID(s)
224+
225+
self._save_model_json()
226+
227+
228+
def sync_model(model: Model, model_filepath: str = "obx-model.json"):
229+
""" Syncs the provided model with the model JSON file. """
230+
231+
id_sync = IdSync(model, model_filepath)
232+
id_sync.sync()

objectbox/model/iduid.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ def __init__(self, id_: int, uid: int):
66
self.uid = uid
77

88
def is_assigned(self):
9-
return self.id != 0 or self.uid != 0
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
1011

1112
def __bool__(self):
1213
return self.is_assigned()

0 commit comments

Comments
 (0)