Skip to content

Commit d51718e

Browse files
committed
Query: link() and backlink() over to-one relations, any depth
1 parent a056c08 commit d51718e

8 files changed

Lines changed: 280 additions & 33 deletions

File tree

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Python binding for the ObjectBox C API (objectbox-c) via `ctypes`; objects are s
3232
- `codegen.py` — experimental, the alternative to `fast_builder.py` (only one is active: `experimental._flatbuffers_mode`): `CodegenBuilder` generates and `exec`s a marshal function per entity that does what `FastBuilder.marshal` does, unrolled; it uses a `FastBuilder`'s layouts. Changes to `FastBuilder.marshal` must be mirrored in the generated code; the same tests check both. It also generates an unmarshal function, used by `_Entity._unmarshal` in codegen mode, that must return the same values as `_unmarshal_table` (flatbuffers.Table, the reference), so changes to reading go into both. Each function is compiled at its first use; with `cache_dir` (`enable_flatbuffers_codegen`), via a .py file in that directory (named by the source hash) and Python's bytecode cache.
3333
- `idsync.py` — assigns IDs/UIDs by syncing the model against an `objectbox-model.json` file (read, matched by UID then name, rewritten from scratch). This is how renames/removals are tracked across schema changes. Matching by name uses `_db_name`, falling back to the other form of the name (e.g. the Python name stored before 5.0), which renames the property and keeps its data.
3434
- **`Store`** (`store.py`) — opening a store: resolve the model (a `Model` or a model name string → collect registered entities), locate `objectbox-model.json` (explicit `model_json_file`, else the calling module's directory found via the call stack, else `__main__`'s directory, else CWD), run `sync_model` (only once per `Model` and JSON file in a process: `Model._synced_json_file`; models for names are cached in `_models_by_name`), then build `StoreOptions` and call `obx_store_open`. `skip_model_sync=True` opens an existing store without passing the model (schema from the database). Directory prefix `memory:` opens an in-memory DB.
35-
- **`Box`** (`box.py`) — CRUD per entity. `box.query(condition)` returns a `QueryBuilder`; conditions (`condition.py`, combinable with `&`/`|`) are applied onto the C query builder, then `.build()` yields a `Query`. `Query.set_parameter(s)()` choose the C function by the property type (`_parameter_functions` in `query.py`); for `set_parameter(s)_alias()`, `QueryBuilder` records the property of each condition alias (`_alias_for_property`).
35+
- **`Box`** (`box.py`) — CRUD per entity. `box.query(condition)` returns a `QueryBuilder`; conditions (`condition.py`, combinable with `&`/`|`) are applied onto the C query builder, then `.build()` yields a `Query`. `Query.set_parameter(s)()` choose the C function by the property type (`_parameter_functions` in `query.py`); for `set_parameter(s)_alias()`, `QueryBuilder` records the entity and property of each condition alias (`_alias_for_property`). `QueryBuilder.link()`/`backlink()` return a builder of the linked entity (`_linked()`: shares the C query builder's lifetime and the aliases; `build()`, `order()` and `close()` raise on it); parameters of linked conditions use the property's entity (`prop._entity`, set by `@Entity()`).
3636
- **Sync** (`sync.py`) — `SyncClient` (created only via `Sync.client()`; its constructor raises) wraps the C sync client (`obx_sync_*`; ctypes callbacks passed to C, also replaced ones, are kept referenced until the client is closed). The store keeps its open client alive (`Store.sync_client()`, like Java and Dart) and closes it (close listener) before the store is closed. `@SyncEntity()` sets the entity's Sync flags.
3737
- **Deprecated API**: `ObjectBox` and `Builder` are thin wrappers around `Store` kept for backward compatibility (`tests/test_deprecated.py`).
3838
- numpy and flatbuffers (which imports numpy) are imported lazily, only where needed (vector/flex properties, the reference FlatBuffers paths); no module-level imports of either (`tests/test_lazy_imports.py`). Use `is_ndarray()` (`c.py`) for type checks and `prop._fb_format` (struct format) instead of `prop._fb_type` outside the reference paths.

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ As this is a beta version, add `--pre` to install it, e.g. `pip install --pre ob
6565
* Order results via `QueryBuilder.order(property, flags)`, e.g. `box.query().order(Person.name).build()`;
6666
`OrderFlags` (exported by the top-level module) adjust the order, e.g. `DESCENDING` or `CASE_SENSITIVE`;
6767
`order_desc(property)` orders descending
68+
* Link queries over to-one relations (`Relation` properties, see below), like in Java and Dart:
69+
`QueryBuilder.link(relation, condition)` matches objects whose related object matches the condition, e.g. orders of
70+
customers named "Ann" via `qb = box.query(); qb.link(Order.customer_id, Customer.name.equals("Ann"))`;
71+
`backlink(relation, condition)` uses a relation in reverse, e.g. customers with a matching order. Both return the
72+
builder of the linked entity to link further (any number of relations); `set_parameter()` also changes conditions
73+
on linked entities.
6874
* New "one of" conditions `String.one_of(values)` and, for integer properties, `one_of(values)` and
6975
`not_one_of(values)`; their values can be changed on a built query via `set_parameter()` (see below)
7076
* New conditions `is_null()` and `is_not_null()` (e.g. `Task.text.is_null()`), also as `QueryBuilder.is_null()`

‎objectbox/c.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,14 @@ def c_array_pointer(py_list: Union[List[Any], np.ndarray], c_type):
956956
# OBX_C_API obx_qb_cond obx_qb_any(OBX_query_builder* builder, const obx_qb_cond conditions[], size_t count);
957957
obx_qb_any = c_fn('obx_qb_any', obx_qb_cond, [OBX_query_builder_p, obx_qb_cond_p, ctypes.c_size_t])
958958

959+
# OBX_C_API OBX_query_builder* obx_qb_link_property(OBX_query_builder* builder, obx_schema_id property_id);
960+
obx_qb_link_property = c_fn('obx_qb_link_property', OBX_query_builder_p, [OBX_query_builder_p, obx_schema_id])
961+
962+
# OBX_C_API OBX_query_builder* obx_qb_backlink_property(OBX_query_builder* builder, obx_schema_id source_entity_id,
963+
# obx_schema_id source_property_id);
964+
obx_qb_backlink_property = c_fn('obx_qb_backlink_property', OBX_query_builder_p,
965+
[OBX_query_builder_p, obx_schema_id, obx_schema_id])
966+
959967
# OBX_C_API obx_err obx_qb_param_alias(OBX_query_builder* builder, const char* alias);
960968
obx_qb_param_alias = c_fn_rc('obx_qb_param_alias', [OBX_query_builder_p, ctypes.c_char_p])
961969

‎objectbox/model/entity.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ def _fill_properties(self):
103103
props_by_db_name = {}
104104
for prop_name, prop in variables.items():
105105
prop.name = prop_name
106+
prop._entity = self
106107
prop._db_name = prop_name if self._python_names else camel_case(prop_name)
107108
other = props_by_db_name.get(prop._db_name)
108109
if other is not None:

‎objectbox/model/properties.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ def __init__(self, pytype: Type, uid: int = 0, **kwargs):
250250
self.iduid = IdUid(0, uid)
251251
self.name = "" # Python attribute name; set in Entity.fill_properties()
252252
self._db_name = "" # Name in the database and model JSON (camelCase by default); set in Entity.fill_properties()
253+
self._entity = None # The _Entity this property belongs to; set in Entity.fill_properties()
253254
self.index = kwargs.get('index', None)
254255

255256
self._py_type = pytype

‎objectbox/query.py‎

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
from objectbox.c import *
2020
from objectbox.exceptions import IllegalArgumentError
21-
from objectbox.model.properties import PropertyType
21+
from objectbox.model.properties import Property, PropertyType
2222
from objectbox.utils import date_value_to_int
2323

2424
if TYPE_CHECKING:
@@ -31,13 +31,13 @@ class Query:
3131
It is created by calling ``Box.query()`` with conditions inside and finalizing with ``build()``.
3232
Note: Query objects are not thread-safe and should not be shared between threads.
3333
"""
34-
def __init__(self, c_query, box: 'Box', alias_property_ids: Optional[Dict[str, int]] = None):
34+
def __init__(self, c_query, box: 'Box', alias_properties: Optional[Dict[str, Tuple['_Entity', int]]] = None):
3535
""" This is an internal constructor. Use ``Box.query()`` instead. """
3636
self._c_query = c_query
3737
self._box = box
3838
self._entity = self._box._entity
3939
self._store = box._store
40-
self._alias_property_ids = alias_property_ids or {}
40+
self._alias_properties = alias_properties or {} # Alias -> (entity, property ID) of the condition
4141

4242
def find(self) -> list:
4343
""" Finds a list of objects matching query. """
@@ -198,9 +198,9 @@ def set_parameter(self, prop, value) -> 'Query':
198198
:raises TypeError:
199199
If the value does not fit the property type.
200200
"""
201-
prop_id = self._entity._get_property_id(prop)
202-
kind, value = _parameter_value(self._property_type(prop_id), value)
203-
return self._set_parameter(prop_id, None, kind, value)
201+
entity, prop_id = self._entity_and_property_id(prop)
202+
kind, value = _parameter_value(self._property_type(entity, prop_id), value)
203+
return self._set_parameter(entity, prop_id, None, kind, value)
204204

205205
@overload
206206
def set_parameter_alias(self, alias: str, value: Union[str, int, float, bytes, datetime]) -> 'Query': ...
@@ -226,7 +226,7 @@ def set_parameter_alias(self, alias: str, value) -> 'Query':
226226
# 0 for non-scalar types (strings, vectors), so it tells integer lists from vectors
227227
type_size = obx_query_param_alias_get_type_size(self._c_query, c_str(alias))
228228
kind, value = _parameter_value(prop_type, value, type_size)
229-
return self._set_parameter(None, alias, kind, value)
229+
return self._set_parameter(None, None, alias, kind, value)
230230

231231
def set_parameters(self, prop: Union[int, str, 'Property'], value_a: Union[int, float, datetime],
232232
value_b: Union[int, float, datetime]) -> 'Query':
@@ -240,46 +240,54 @@ def set_parameters(self, prop: Union[int, str, 'Property'], value_a: Union[int,
240240
:raises TypeError:
241241
If the values do not fit the property type.
242242
"""
243-
prop_id = self._entity._get_property_id(prop)
244-
kind, value_a, value_b = _parameter_values(self._property_type(prop_id), value_a, value_b)
245-
return self._set_parameter(prop_id, None, kind, value_a, value_b)
243+
entity, prop_id = self._entity_and_property_id(prop)
244+
kind, value_a, value_b = _parameter_values(self._property_type(entity, prop_id), value_a, value_b)
245+
return self._set_parameter(entity, prop_id, None, kind, value_a, value_b)
246246

247247
def set_parameters_alias(self, alias: str, value_a: Union[int, float, datetime],
248248
value_b: Union[int, float, datetime]) -> 'Query':
249249
""" Like :meth:`set_parameters`, for the condition having the given alias
250250
(see ``PropertyQueryCondition.alias()``). """
251251
kind, value_a, value_b = _parameter_values(self._alias_property_type(alias), value_a, value_b)
252-
return self._set_parameter(None, alias, kind, value_a, value_b)
252+
return self._set_parameter(None, None, alias, kind, value_a, value_b)
253253

254254
def describe_parameters(self) -> str:
255255
""" Describes the query conditions with their current parameter values, e.g. for logging or debugging. """
256256
return obx_query_describe_params(self._c_query).decode('utf-8')
257257

258-
def _set_parameter(self, prop_id: Optional[int], alias: Optional[str], kind: str, *values) -> 'Query':
258+
def _set_parameter(self, entity, prop_id: Optional[int], alias: Optional[str], kind: str, *values) -> 'Query':
259259
""" Calls the C function for the kind of parameter (see _parameter_functions) for the property or alias. """
260260
if kind == "ints": # The C API has separate functions for 32-bit and 64-bit integer properties
261261
if alias is None:
262-
type_size = obx_query_param_get_type_size(self._c_query, self._entity._id, prop_id)
262+
type_size = obx_query_param_get_type_size(self._c_query, entity._id, prop_id)
263263
else:
264264
type_size = obx_query_param_alias_get_type_size(self._c_query, c_str(alias))
265265
kind = "int32s" if type_size == 4 else "int64s"
266266
by_property, by_alias, to_c_args = _parameter_functions[kind]
267267
if alias is None:
268-
by_property(self._c_query, self._entity._id, prop_id, *to_c_args(*values))
268+
by_property(self._c_query, entity._id, prop_id, *to_c_args(*values))
269269
else:
270270
by_alias(self._c_query, c_str(alias), *to_c_args(*values))
271271
return self
272272

273-
def _property_type(self, prop_id: int) -> int:
274-
for prop in self._entity._properties:
273+
def _entity_and_property_id(self, prop) -> Tuple['_Entity', int]:
274+
""" The entity is the query's, or another one's for its Property, e.g. of a linked entity (see
275+
QueryBuilder.link()). """
276+
if isinstance(prop, Property) and prop._entity is not None:
277+
return prop._entity, prop.id
278+
return self._entity, self._entity._get_property_id(prop)
279+
280+
@staticmethod
281+
def _property_type(entity, prop_id: int) -> int:
282+
for prop in entity._properties:
275283
if prop.id == prop_id:
276284
return prop._ob_type
277-
raise IllegalArgumentError(f"Property ID {prop_id} not found in entity {self._entity._name}")
285+
raise IllegalArgumentError(f"Property ID {prop_id} not found in entity {entity._name}")
278286

279287
def _alias_property_type(self, alias: str) -> Optional[int]:
280288
""" None if the property is unknown, i.e. the alias was set via QueryBuilder.alias(). """
281-
prop_id = self._alias_property_ids.get(alias)
282-
return self._property_type(prop_id) if prop_id is not None else None
289+
entity_and_prop_id = self._alias_properties.get(alias)
290+
return self._property_type(*entity_and_prop_id) if entity_and_prop_id is not None else None
283291

284292

285293
# Kind of parameter: (C function by property, C function by alias, converts the value(s) to the C arguments)

0 commit comments

Comments
 (0)