Skip to content

Commit 8d16afb

Browse files
committed
Merge branch '65-add-query-functionalities' into dev
2 parents aec3140 + fec1245 commit 8d16afb

9 files changed

Lines changed: 848 additions & 70 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.
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`.
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`).
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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ ObjectBox Python ChangeLog
2828
**Breaking:** this replaces `CoreException` and `NotFoundException` (the latter was exported by the top-level module).
2929
`IllegalArgumentError` is also a `ValueError`, `IllegalStateError` and `FeatureNotAvailable` are also a
3030
`RuntimeError`, so they can be caught as the matching built-in exception, too.
31+
* Query: order results via `QueryBuilder.order(property, flags)`, e.g. `box.query().order(Person.name).build()`;
32+
`OrderFlags` (exported by the top-level module) adjust the order, e.g. `DESCENDING` or `CASE_SENSITIVE`;
33+
`order_desc(property)` orders descending
34+
* Query: new "one of" conditions `String.one_of(values)` and, for integer properties, `one_of(values)` and
35+
`not_one_of(values)`; their values can be changed on a built query via `set_parameter()` (see below)
36+
* Query: `set_parameter(property, value)` and `set_parameter_alias(alias, value)` change the value of a condition;
37+
the property type decides how, e.g. a list sets the vector of a nearest neighbor search or the values of a
38+
"one of" condition, and `Date` properties also take a `datetime`. `set_parameters(property, a, b)` and
39+
`set_parameters_alias(alias, a, b)` change the two values of `between()`. New are also parameters for floating point
40+
and bytes properties, and `describe_parameters()`.
41+
**Breaking:** the typed methods `set_parameter_string()`, `set_parameter_int()` and `set_parameter_vector_f32()` are
42+
removed; use `set_parameter()` instead, and `set_parameter_alias()` for their `set_parameter_alias_*()` variants.
3143
* Store: new `log_callback` option to receive the database's log messages
3244
* Top-level module now also exports `Query`, `QueryBuilder`, `PropertyQueryCondition` and `HnswFlags`
3345
* API reference documentation (docstrings for the public API; generated via Sphinx)

‎objectbox/__init__.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from objectbox.query_builder import QueryBuilder
2828
from objectbox.builder import Builder
2929
from objectbox.objectbox import ObjectBox
30+
from objectbox.query_builder import OrderFlags
3031

3132
__all__ = [
3233
'Box',
@@ -77,7 +78,8 @@
7778
'QueryBuilder',
7879
'SyncEntity',
7980
'SyncClock',
80-
'SyncPrecedence'
81+
'SyncPrecedence',
82+
'OrderFlags'
8183
]
8284

8385
# Python binding version

‎objectbox/c.py‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -976,6 +976,84 @@ def c_array_pointer(py_list: Union[List[Any], np.ndarray], c_type):
976976
# OBX_C_API obx_err obx_query_param_alias_int(OBX_query* query, const char* alias, int64_t value);
977977
obx_query_param_alias_int = c_fn_rc('obx_query_param_alias_int', [OBX_query_p, ctypes.c_char_p, ctypes.c_int64])
978978

979+
# OBX_C_API obx_err obx_query_param_strings(OBX_query* query, obx_schema_id entity_id, obx_schema_id property_id,
980+
# const char* const values[], size_t count);
981+
obx_query_param_strings = c_fn_rc('obx_query_param_strings',
982+
[OBX_query_p, obx_schema_id, obx_schema_id, ctypes.POINTER(ctypes.c_char_p),
983+
ctypes.c_size_t])
984+
985+
# OBX_C_API obx_err obx_query_param_int64s(OBX_query* query, obx_schema_id entity_id, obx_schema_id property_id,
986+
# const int64_t values[], size_t count);
987+
obx_query_param_int64s = c_fn_rc('obx_query_param_int64s',
988+
[OBX_query_p, obx_schema_id, obx_schema_id, ctypes.POINTER(ctypes.c_int64),
989+
ctypes.c_size_t])
990+
991+
# OBX_C_API obx_err obx_query_param_int32s(OBX_query* query, obx_schema_id entity_id, obx_schema_id property_id,
992+
# const int32_t values[], size_t count);
993+
obx_query_param_int32s = c_fn_rc('obx_query_param_int32s',
994+
[OBX_query_p, obx_schema_id, obx_schema_id, ctypes.POINTER(ctypes.c_int32),
995+
ctypes.c_size_t])
996+
997+
# OBX_C_API obx_err obx_query_param_alias_strings(OBX_query* query, const char* alias, const char* const values[],
998+
# size_t count);
999+
obx_query_param_alias_strings = c_fn_rc('obx_query_param_alias_strings',
1000+
[OBX_query_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p),
1001+
ctypes.c_size_t])
1002+
1003+
# OBX_C_API obx_err obx_query_param_alias_int64s(OBX_query* query, const char* alias, const int64_t values[],
1004+
# size_t count);
1005+
obx_query_param_alias_int64s = c_fn_rc('obx_query_param_alias_int64s',
1006+
[OBX_query_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_int64), ctypes.c_size_t])
1007+
1008+
# OBX_C_API obx_err obx_query_param_alias_int32s(OBX_query* query, const char* alias, const int32_t values[],
1009+
# size_t count);
1010+
obx_query_param_alias_int32s = c_fn_rc('obx_query_param_alias_int32s',
1011+
[OBX_query_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_int32), ctypes.c_size_t])
1012+
1013+
# OBX_C_API obx_err obx_query_param_2ints(OBX_query* query, obx_schema_id entity_id, obx_schema_id property_id,
1014+
# int64_t value_a, int64_t value_b);
1015+
obx_query_param_2ints = c_fn_rc('obx_query_param_2ints',
1016+
[OBX_query_p, obx_schema_id, obx_schema_id, ctypes.c_int64, ctypes.c_int64])
1017+
1018+
# OBX_C_API obx_err obx_query_param_double(OBX_query* query, obx_schema_id entity_id, obx_schema_id property_id,
1019+
# double value);
1020+
obx_query_param_double = c_fn_rc('obx_query_param_double', [OBX_query_p, obx_schema_id, obx_schema_id, ctypes.c_double])
1021+
1022+
# OBX_C_API obx_err obx_query_param_2doubles(OBX_query* query, obx_schema_id entity_id, obx_schema_id property_id,
1023+
# double value_a, double value_b);
1024+
obx_query_param_2doubles = c_fn_rc('obx_query_param_2doubles',
1025+
[OBX_query_p, obx_schema_id, obx_schema_id, ctypes.c_double, ctypes.c_double])
1026+
1027+
# OBX_C_API obx_err obx_query_param_bytes(OBX_query* query, obx_schema_id entity_id, obx_schema_id property_id,
1028+
# const void* value, size_t size);
1029+
obx_query_param_bytes = c_fn_rc('obx_query_param_bytes',
1030+
[OBX_query_p, obx_schema_id, obx_schema_id, ctypes.c_void_p, ctypes.c_size_t])
1031+
1032+
# OBX_C_API obx_err obx_query_param_alias_2ints(OBX_query* query, const char* alias, int64_t value_a, int64_t value_b);
1033+
obx_query_param_alias_2ints = c_fn_rc('obx_query_param_alias_2ints',
1034+
[OBX_query_p, ctypes.c_char_p, ctypes.c_int64, ctypes.c_int64])
1035+
1036+
# OBX_C_API obx_err obx_query_param_alias_double(OBX_query* query, const char* alias, double value);
1037+
obx_query_param_alias_double = c_fn_rc('obx_query_param_alias_double', [OBX_query_p, ctypes.c_char_p, ctypes.c_double])
1038+
1039+
# OBX_C_API obx_err obx_query_param_alias_2doubles(OBX_query* query, const char* alias, double value_a, double value_b);
1040+
obx_query_param_alias_2doubles = c_fn_rc('obx_query_param_alias_2doubles',
1041+
[OBX_query_p, ctypes.c_char_p, ctypes.c_double, ctypes.c_double])
1042+
1043+
# OBX_C_API obx_err obx_query_param_alias_bytes(OBX_query* query, const char* alias, const void* value, size_t size);
1044+
obx_query_param_alias_bytes = c_fn_rc('obx_query_param_alias_bytes',
1045+
[OBX_query_p, ctypes.c_char_p, ctypes.c_void_p, ctypes.c_size_t])
1046+
1047+
# OBX_C_API size_t obx_query_param_get_type_size(OBX_query* query, obx_schema_id entity_id, obx_schema_id property_id);
1048+
# Not checked: returns 0 for non-scalar types (e.g. strings) and on errors.
1049+
obx_query_param_get_type_size = c_fn_nocheck('obx_query_param_get_type_size', ctypes.c_size_t,
1050+
[OBX_query_p, obx_schema_id, obx_schema_id])
1051+
1052+
# OBX_C_API size_t obx_query_param_alias_get_type_size(OBX_query* query, const char* alias);
1053+
# Not checked: returns 0 for non-scalar types (e.g. strings) and on errors.
1054+
obx_query_param_alias_get_type_size = c_fn_nocheck('obx_query_param_alias_get_type_size', ctypes.c_size_t,
1055+
[OBX_query_p, ctypes.c_char_p])
1056+
9791057
# OBX_C_API obx_err obx_qb_order(OBX_query_builder* builder, obx_schema_id property_id, OBXOrderFlags flags);
9801058
obx_qb_order = c_fn_rc('obx_qb_order', [OBX_query_builder_p, obx_schema_id, OBXOrderFlags])
9811059

‎objectbox/condition.py‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ class PropertyQueryConditionOp(Enum):
8888
CONTAINS_KEY_VALUE = 12
8989
IS_NULL = 13
9090
NOT_NULL = 14
91+
ONE_OF = 15
92+
NOT_ONE_OF = 16
9193

9294

9395
class PropertyQueryCondition(QueryCondition):
@@ -112,6 +114,8 @@ class PropertyQueryCondition(QueryCondition):
112114
PropertyQueryConditionOp.CONTAINS_KEY_VALUE: "_contains_key_value",
113115
PropertyQueryConditionOp.IS_NULL: "_apply_is_null",
114116
PropertyQueryConditionOp.NOT_NULL: "_apply_not_null",
117+
PropertyQueryConditionOp.ONE_OF: "_apply_one_of",
118+
PropertyQueryConditionOp.NOT_ONE_OF: "_apply_not_one_of",
115119
# ... new property query conditions here ... :)
116120
}
117121

@@ -130,7 +134,8 @@ def __init__(self, property_id: int, op: PropertyQueryConditionOp, args: Dict[st
130134
self._alias = None
131135

132136
def alias(self, value: str):
133-
""" Sets an alias for this condition that can later be used with Query's set_parameter_* methods. """
137+
""" Sets an alias for this condition to change its value(s) on the built query via
138+
``Query.set_parameter_alias()`` or ``Query.set_parameters_alias()``. """
134139
self._alias = value
135140
return self
136141

@@ -274,8 +279,21 @@ def _apply_is_null(self, qb: QueryBuilder) -> obx_qb_cond:
274279
def _apply_not_null(self, qb: QueryBuilder) -> obx_qb_cond:
275280
return qb.not_null(self._property_id)
276281

282+
def _apply_one_of(self, qb: QueryBuilder) -> obx_qb_cond:
283+
values = self._args['values']
284+
# Cannot tell strings from ints by the values (the list may be empty), so use the property kind
285+
if self._args.get('string', False):
286+
case_sensitive = self._args['case_sensitive']
287+
return qb.in_strings(self._property_id, values, case_sensitive)
288+
else:
289+
return qb.in_ints(self._property_id, values)
290+
291+
def _apply_not_one_of(self, qb: QueryBuilder) -> obx_qb_cond:
292+
values = self._args['values']
293+
return qb.not_in_ints(self._property_id, values)
294+
277295
def apply(self, qb: QueryBuilder) -> obx_qb_cond:
278296
c_cond = self._op_func(qb)
279297
if self._alias is not None:
280-
qb.alias(self._alias)
298+
qb._alias_for_property(self._alias, self._property_id)
281299
return c_cond

‎objectbox/model/properties.py‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,32 @@ def not_equals(self, value) -> PropertyQueryCondition:
394394
args = {'value': value}
395395
return PropertyQueryCondition(self.id, PropertyQueryConditionOp.NOT_EQ, args)
396396

397+
def one_of(self, values: List[int]) -> PropertyQueryCondition:
398+
""" *one of* (``in``) condition to be passed to :func:`Box.query`;
399+
matches if the property value equals any of the given values.
400+
Supported for 32-bit (Int32) and 64-bit (Int64, Id, Date, DateNano) integer properties;
401+
raises a ``TypeError`` for other properties (e.g. Int8, Int16, Bool). """
402+
self._assert_ids_assigned()
403+
self._assert_one_of_supported()
404+
args = {'values': values}
405+
return PropertyQueryCondition(self.id, PropertyQueryConditionOp.ONE_OF, args)
406+
407+
def not_one_of(self, values: List[int]) -> PropertyQueryCondition:
408+
""" *not one of* (``not in``) condition to be passed to :func:`Box.query`;
409+
matches if the property value equals none of the given values.
410+
Supported for 32-bit (Int32) and 64-bit (Int64, Id, Date, DateNano) integer properties;
411+
raises a ``TypeError`` for other properties (e.g. Int8, Int16, Bool). """
412+
self._assert_ids_assigned()
413+
self._assert_one_of_supported()
414+
args = {'values': values}
415+
return PropertyQueryCondition(self.id, PropertyQueryConditionOp.NOT_ONE_OF, args)
416+
417+
def _assert_one_of_supported(self):
418+
# The C API only supports 32-bit and 64-bit integers for "in" conditions
419+
if self._ob_type not in (PropertyType.int, PropertyType.long, PropertyType.date, PropertyType.dateNano):
420+
raise TypeError(f"Property \"{self.name}\" of type {PropertyType(self._ob_type).name} "
421+
f"does not support one_of()/not_one_of(); supported are 32-bit and 64-bit integers")
422+
397423

398424
# ID property (primary key)
399425
class Id(_IntProperty):
@@ -466,8 +492,13 @@ def less_or_equal(self, value, case_sensitive: bool = True) -> PropertyQueryCond
466492
self._assert_ids_assigned()
467493
args = {'value': value, 'case_sensitive': case_sensitive}
468494
return PropertyQueryCondition(self.id, PropertyQueryConditionOp.LTE, args)
469-
470495

496+
def one_of(self, values: List[str], case_sensitive: bool = True) -> PropertyQueryCondition:
497+
""" *one of* (``in``) condition (opt-in: case-sensitive) to be passed to :func:`Box.query`;
498+
matches if the property value equals any of the given values. """
499+
self._assert_ids_assigned()
500+
args = {'values': values, 'case_sensitive': case_sensitive, 'string': True}
501+
return PropertyQueryCondition(self.id, PropertyQueryConditionOp.ONE_OF, args)
471502

472503
# Signed Integer Numeric Properties
473504
class Int8(_IntProperty):

0 commit comments

Comments
 (0)