Skip to content

Commit 8011ec3

Browse files
committed
Merge branch '39-logic-query-condition' into 'dev'
Resolve "Logical aggregation of QueryCondition's (and/or)" #39 Closes #39 See merge request objectbox/objectbox-python!27
2 parents 2f6d8ac + 12aeeb0 commit 8011ec3

4 files changed

Lines changed: 222 additions & 86 deletions

File tree

objectbox/condition.py

Lines changed: 117 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,64 @@
1+
from __future__ import annotations
2+
13
from enum import Enum
24
from typing import *
35
import numpy as np
46

7+
if TYPE_CHECKING:
8+
from objectbox.c import obx_qb_cond
9+
from objectbox.query_builder import QueryBuilder
10+
11+
12+
class QueryCondition:
13+
def and_(self, other: QueryCondition) -> QueryCondition:
14+
return LogicQueryCondition(self, other, LogicQueryConditionOp.AND)
15+
__and__ = and_
16+
17+
def or_(self, other: QueryCondition) -> QueryCondition:
18+
return LogicQueryCondition(self, other, LogicQueryConditionOp.OR)
19+
__or__ = or_
20+
21+
def apply(self, qb: QueryBuilder) -> obx_qb_cond:
22+
""" Applies the QueryCondition to the supplied QueryBuilder.
23+
24+
:return:
25+
The C handle for the applied condition.
26+
"""
27+
raise NotImplementedError
28+
29+
30+
class LogicQueryConditionOp(Enum):
31+
AND = 1
32+
OR = 2
33+
34+
35+
class LogicQueryCondition(QueryCondition):
36+
""" A QueryCondition describing a logical operation between two inner QueryCondition's (e.g. AND/OR). """
537

6-
class _QueryConditionOp(Enum):
38+
def __init__(self, cond1: QueryCondition, cond2: QueryCondition, op: LogicQueryConditionOp):
39+
self._cond1 = cond1
40+
self._cond2 = cond2
41+
self._op = op
42+
43+
def _apply_conditions(self, qb: QueryBuilder) -> List[obx_qb_cond]:
44+
return [self._cond1.apply(qb), self._cond2.apply(qb)]
45+
46+
def _apply_and(self, qb: QueryBuilder) -> obx_qb_cond:
47+
return qb.all(self._apply_conditions(qb))
48+
49+
def _apply_or(self, qb: QueryBuilder) -> obx_qb_cond:
50+
return qb.any(self._apply_conditions(qb))
51+
52+
def apply(self, qb: QueryBuilder) -> obx_qb_cond:
53+
if self._op == LogicQueryConditionOp.AND:
54+
return self._apply_and(qb)
55+
elif self._op == LogicQueryConditionOp.OR:
56+
return self._apply_or(qb)
57+
else:
58+
raise Exception(f"Unknown LogicQueryCondition op: {self._op.name}")
59+
60+
61+
class PropertyQueryConditionOp(Enum):
762
EQ = 1
863
NOT_EQ = 2
964
CONTAINS = 3
@@ -18,130 +73,137 @@ class _QueryConditionOp(Enum):
1873
CONTAINS_KEY_VALUE = 12
1974

2075

21-
class QueryCondition:
22-
def __init__(self, property_id: int, op: _QueryConditionOp, args: Dict[str, Any]):
23-
if op not in self._get_op_map():
24-
raise Exception(f"Invalid query condition op with ID: {op}")
76+
class PropertyQueryCondition(QueryCondition):
77+
""" A QueryCondition describing an operation to be applied on a property (e.g. name == "John", age == 24) """
78+
79+
_OP_MAP: Dict[PropertyQueryConditionOp, str] = {
80+
PropertyQueryConditionOp.EQ: "_apply_eq",
81+
PropertyQueryConditionOp.NOT_EQ: "_apply_not_eq",
82+
PropertyQueryConditionOp.CONTAINS: "_apply_contains",
83+
PropertyQueryConditionOp.STARTS_WITH: "_apply_starts_with",
84+
PropertyQueryConditionOp.ENDS_WITH: "_apply_ends_with",
85+
PropertyQueryConditionOp.GT: "_apply_gt",
86+
PropertyQueryConditionOp.GTE: "_apply_gte",
87+
PropertyQueryConditionOp.LT: "_apply_lt",
88+
PropertyQueryConditionOp.LTE: "_apply_lte",
89+
PropertyQueryConditionOp.BETWEEN: "_apply_between",
90+
PropertyQueryConditionOp.NEAREST_NEIGHBOR: "_apply_nearest_neighbor",
91+
PropertyQueryConditionOp.CONTAINS_KEY_VALUE: "_contains_key_value"
92+
# ... new property query conditions here ... :)
93+
}
94+
95+
def __init__(self, property_id: int, op: PropertyQueryConditionOp, args: Dict[str, Any]):
96+
if op not in self._OP_MAP:
97+
raise Exception(f"Invalid PropertyQueryConditionOp: {op}")
98+
op_func_name = self._OP_MAP[op]
99+
if not hasattr(self, op_func_name):
100+
raise Exception(f"Missing PropertyQueryCondition op function: {op_func_name} (op: {op})")
101+
op_func = getattr(self, op_func_name)
25102

26103
self._property_id = property_id
27104
self._op = op
105+
self._op_func = op_func
28106
self._args = args
29107
self._alias = None
30108

31109
def alias(self, value: str):
110+
""" Sets an alias for this condition that can later be used with Query's set_parameter_* methods. """
32111
self._alias = value
33112
return self
34113

35-
def _get_op_map(self):
36-
return {
37-
_QueryConditionOp.EQ: self._apply_eq,
38-
_QueryConditionOp.NOT_EQ: self._apply_not_eq,
39-
_QueryConditionOp.CONTAINS: self._apply_contains,
40-
_QueryConditionOp.STARTS_WITH: self._apply_starts_with,
41-
_QueryConditionOp.ENDS_WITH: self._apply_ends_with,
42-
_QueryConditionOp.GT: self._apply_gt,
43-
_QueryConditionOp.GTE: self._apply_gte,
44-
_QueryConditionOp.LT: self._apply_lt,
45-
_QueryConditionOp.LTE: self._apply_lte,
46-
_QueryConditionOp.BETWEEN: self._apply_between,
47-
_QueryConditionOp.NEAREST_NEIGHBOR: self._apply_nearest_neighbor,
48-
_QueryConditionOp.CONTAINS_KEY_VALUE: self._contains_key_value
49-
# ... new query condition here ... :)
50-
}
51-
52-
def _apply_eq(self, qb: 'QueryBuilder'):
114+
def _apply_eq(self, qb: QueryBuilder) -> obx_qb_cond:
53115
value = self._args['value']
54116
case_sensitive = self._args['case_sensitive']
55117
if isinstance(value, str):
56-
qb.equals_string(self._property_id, value, case_sensitive)
118+
return qb.equals_string(self._property_id, value, case_sensitive)
57119
elif isinstance(value, int):
58-
qb.equals_int(self._property_id, value)
120+
return qb.equals_int(self._property_id, value)
59121
else:
60122
raise Exception(f"Unsupported type for 'EQ': {type(value)}")
61123

62-
def _apply_not_eq(self, qb: 'QueryBuilder'):
124+
def _apply_not_eq(self, qb: QueryBuilder) -> obx_qb_cond:
63125
value = self._args['value']
64126
case_sensitive = self._args['case_sensitive']
65127
if isinstance(value, str):
66-
qb.not_equals_string(self._property_id, value, case_sensitive)
128+
return qb.not_equals_string(self._property_id, value, case_sensitive)
67129
elif isinstance(value, int):
68-
qb.not_equals_int(self._property_id, value)
130+
return qb.not_equals_int(self._property_id, value)
69131
else:
70132
raise Exception(f"Unsupported type for 'NOT_EQ': {type(value)}")
71133

72-
def _apply_contains(self, qb: 'QueryBuilder'):
134+
def _apply_contains(self, qb: QueryBuilder) -> obx_qb_cond:
73135
value = self._args['value']
74136
case_sensitive = self._args['case_sensitive']
75137
if isinstance(value, str):
76-
qb.contains_string(self._property_id, value, case_sensitive)
138+
return qb.contains_string(self._property_id, value, case_sensitive)
77139
else:
78140
raise Exception(f"Unsupported type for 'CONTAINS': {type(value)}")
79141

80-
def _apply_starts_with(self, qb: 'QueryBuilder'):
142+
def _apply_starts_with(self, qb: QueryBuilder) -> obx_qb_cond:
81143
value = self._args['value']
82144
case_sensitive = self._args['case_sensitive']
83145
if isinstance(value, str):
84-
qb.starts_with_string(self._property_id, value, case_sensitive)
146+
return qb.starts_with_string(self._property_id, value, case_sensitive)
85147
else:
86148
raise Exception(f"Unsupported type for 'STARTS_WITH': {type(value)}")
87149

88-
def _apply_ends_with(self, qb: 'QueryBuilder'):
150+
def _apply_ends_with(self, qb: QueryBuilder) -> obx_qb_cond:
89151
value = self._args['value']
90152
case_sensitive = self._args['case_sensitive']
91153
if isinstance(value, str):
92-
qb.ends_with_string(self._property_id, value, case_sensitive)
154+
return qb.ends_with_string(self._property_id, value, case_sensitive)
93155
else:
94156
raise Exception(f"Unsupported type for 'ENDS_WITH': {type(value)}")
95157

96-
def _apply_gt(self, qb: 'QueryBuilder'):
158+
def _apply_gt(self, qb: QueryBuilder) -> obx_qb_cond:
97159
value = self._args['value']
98160
case_sensitive = self._args['case_sensitive']
99161
if isinstance(value, str):
100-
qb.greater_than_string(self._property_id, value, case_sensitive)
162+
return qb.greater_than_string(self._property_id, value, case_sensitive)
101163
elif isinstance(value, int):
102-
qb.greater_than_int(self._property_id, value)
164+
return qb.greater_than_int(self._property_id, value)
103165
else:
104166
raise Exception(f"Unsupported type for 'GT': {type(value)}")
105167

106-
def _apply_gte(self, qb: 'QueryBuilder'):
168+
def _apply_gte(self, qb: QueryBuilder) -> obx_qb_cond:
107169
value = self._args['value']
108170
case_sensitive = self._args['case_sensitive']
109171
if isinstance(value, str):
110-
qb.greater_or_equal_string(self._property_id, value, case_sensitive)
172+
return qb.greater_or_equal_string(self._property_id, value, case_sensitive)
111173
elif isinstance(value, int):
112-
qb.greater_or_equal_int(self._property_id, value)
174+
return qb.greater_or_equal_int(self._property_id, value)
113175
else:
114176
raise Exception(f"Unsupported type for 'GTE': {type(value)}")
115177

116-
def _apply_lt(self, qb: 'QueryCondition'):
178+
def _apply_lt(self, qb: QueryBuilder) -> obx_qb_cond:
117179
value = self._args['value']
118180
case_sensitive = self._args['case_sensitive']
119181
if isinstance(value, str):
120-
qb.less_than_string(self._property_id, value, case_sensitive)
182+
return qb.less_than_string(self._property_id, value, case_sensitive)
121183
elif isinstance(value, int):
122-
qb.less_than_int(self._property_id, value)
184+
return qb.less_than_int(self._property_id, value)
123185
else:
124186
raise Exception("Unsupported type for 'LT': " + str(type(value)))
125187

126-
def _apply_lte(self, qb: 'QueryBuilder'):
188+
def _apply_lte(self, qb: QueryBuilder) -> obx_qb_cond:
127189
value = self._args['value']
128190
case_sensitive = self._args['case_sensitive']
129191
if isinstance(value, str):
130-
qb.less_or_equal_string(self._property_id, value, case_sensitive)
192+
return qb.less_or_equal_string(self._property_id, value, case_sensitive)
131193
elif isinstance(value, int):
132-
qb.less_or_equal_int(self._property_id, value)
194+
return qb.less_or_equal_int(self._property_id, value)
133195
else:
134196
raise Exception(f"Unsupported type for 'LTE': {type(value)}")
135197

136-
def _apply_between(self, qb: 'QueryBuilder'):
198+
def _apply_between(self, qb: QueryBuilder) -> obx_qb_cond:
137199
a = self._args['a']
138200
b = self._args['b']
139201
if isinstance(a, int):
140-
qb.between_2ints(self._property_id, a, b)
202+
return qb.between_2ints(self._property_id, a, b)
141203
else:
142204
raise Exception(f"Unsupported type for 'BETWEEN': {type(a)}")
143205

144-
def _apply_nearest_neighbor(self, qb: 'QueryBuilder'):
206+
def _apply_nearest_neighbor(self, qb: QueryBuilder) -> obx_qb_cond:
145207
query_vector = self._args['query_vector']
146208
element_count = self._args['element_count']
147209

@@ -152,19 +214,18 @@ def _apply_nearest_neighbor(self, qb: 'QueryBuilder'):
152214
is_float_vector |= isinstance(query_vector, np.ndarray) and query_vector.dtype == np.float32
153215
is_float_vector |= isinstance(query_vector, list) and type(query_vector[0]) == float
154216
if is_float_vector:
155-
qb.nearest_neighbors_f32(self._property_id, query_vector, element_count)
217+
return qb.nearest_neighbors_f32(self._property_id, query_vector, element_count)
156218
else:
157219
raise Exception(f"Unsupported type for 'NEAREST_NEIGHBOR': {type(query_vector)}")
158220

159-
def _contains_key_value(self, qb: 'QueryBuilder'):
221+
def _contains_key_value(self, qb: QueryBuilder) -> obx_qb_cond:
160222
key = self._args['key']
161223
value = self._args['value']
162224
case_sensitive = self._args['case_sensitive']
163-
qb.contains_key_value(self._property_id, key, value, case_sensitive)
164-
165-
def apply(self, qb: 'QueryBuilder'):
166-
""" Applies the stored condition to the supplied query builder. """
167-
self._get_op_map()[self._op](qb)
225+
return qb.contains_key_value(self._property_id, key, value, case_sensitive)
168226

227+
def apply(self, qb: QueryBuilder) -> obx_qb_cond:
228+
c_cond = self._op_func(qb)
169229
if self._alias is not None:
170230
qb.alias(self._alias)
231+
return c_cond

objectbox/model/properties.py

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@
1313
# limitations under the License.
1414

1515
from enum import IntEnum
16-
17-
from objectbox.condition import QueryCondition, _QueryConditionOp
18-
from objectbox.c import *
1916
import flatbuffers.number_types
2017
import numpy as np
2118
from dataclasses import dataclass
2219

20+
from objectbox.c import *
21+
from objectbox.condition import PropertyQueryCondition, PropertyQueryConditionOp
22+
23+
2324
class PropertyType(IntEnum):
2425
bool = OBXPropertyType_Bool
2526
byte = OBXPropertyType_Byte
@@ -182,53 +183,53 @@ def _set_flags(self):
182183
if isinstance(self._index, Index): # Generic index
183184
self._flags |= self._index.type
184185

185-
def equals(self, value, case_sensitive: bool = True) -> QueryCondition:
186+
def equals(self, value, case_sensitive: bool = True) -> PropertyQueryCondition:
186187
args = {'value': value, 'case_sensitive': case_sensitive}
187-
return QueryCondition(self._id, _QueryConditionOp.EQ, args)
188+
return PropertyQueryCondition(self._id, PropertyQueryConditionOp.EQ, args)
188189

189-
def not_equals(self, value, case_sensitive: bool = True) -> QueryCondition:
190+
def not_equals(self, value, case_sensitive: bool = True) -> PropertyQueryCondition:
190191
args = {'value': value, 'case_sensitive': case_sensitive}
191-
return QueryCondition(self._id, _QueryConditionOp.NOT_EQ, args)
192+
return PropertyQueryCondition(self._id, PropertyQueryConditionOp.NOT_EQ, args)
192193

193-
def contains(self, value: str, case_sensitive: bool = True) -> QueryCondition:
194+
def contains(self, value: str, case_sensitive: bool = True) -> PropertyQueryCondition:
194195
args = {'value': value, 'case_sensitive': case_sensitive}
195-
return QueryCondition(self._id, _QueryConditionOp.CONTAINS, args)
196+
return PropertyQueryCondition(self._id, PropertyQueryConditionOp.CONTAINS, args)
196197

197-
def starts_with(self, value: str, case_sensitive: bool = True) -> QueryCondition:
198+
def starts_with(self, value: str, case_sensitive: bool = True) -> PropertyQueryCondition:
198199
args = {'value': value, 'case_sensitive': case_sensitive}
199-
return QueryCondition(self._id, _QueryConditionOp.STARTS_WITH, args)
200+
return PropertyQueryCondition(self._id, PropertyQueryConditionOp.STARTS_WITH, args)
200201

201-
def ends_with(self, value: str, case_sensitive: bool = True) -> QueryCondition:
202+
def ends_with(self, value: str, case_sensitive: bool = True) -> PropertyQueryCondition:
202203
args = {'value': value, 'case_sensitive': case_sensitive}
203-
return QueryCondition(self._id, _QueryConditionOp.ENDS_WITH, args)
204+
return PropertyQueryCondition(self._id, PropertyQueryConditionOp.ENDS_WITH, args)
204205

205-
def greater_than(self, value, case_sensitive: bool = True) -> QueryCondition:
206+
def greater_than(self, value, case_sensitive: bool = True) -> PropertyQueryCondition:
206207
args = {'value': value, 'case_sensitive': case_sensitive}
207-
return QueryCondition(self._id, _QueryConditionOp.GT, args)
208+
return PropertyQueryCondition(self._id, PropertyQueryConditionOp.GT, args)
208209

209-
def greater_or_equal(self, value, case_sensitive: bool = True) -> QueryCondition:
210+
def greater_or_equal(self, value, case_sensitive: bool = True) -> PropertyQueryCondition:
210211
args = {'value': value, 'case_sensitive': case_sensitive}
211-
return QueryCondition(self._id, _QueryConditionOp.GTE, args)
212+
return PropertyQueryCondition(self._id, PropertyQueryConditionOp.GTE, args)
212213

213-
def less_than(self, value, case_sensitive: bool = True) -> QueryCondition:
214+
def less_than(self, value, case_sensitive: bool = True) -> PropertyQueryCondition:
214215
args = {'value': value, 'case_sensitive': case_sensitive}
215-
return QueryCondition(self._id, _QueryConditionOp.LT, args)
216+
return PropertyQueryCondition(self._id, PropertyQueryConditionOp.LT, args)
216217

217-
def less_or_equal(self, value, case_sensitive: bool = True) -> QueryCondition:
218+
def less_or_equal(self, value, case_sensitive: bool = True) -> PropertyQueryCondition:
218219
args = {'value': value, 'case_sensitive': case_sensitive}
219-
return QueryCondition(self._id, _QueryConditionOp.LTE, args)
220+
return PropertyQueryCondition(self._id, PropertyQueryConditionOp.LTE, args)
220221

221-
def between(self, a, b) -> QueryCondition:
222+
def between(self, a, b) -> PropertyQueryCondition:
222223
args = {'a': a, 'b': b}
223-
return QueryCondition(self._id, _QueryConditionOp.BETWEEN, args)
224+
return PropertyQueryCondition(self._id, PropertyQueryConditionOp.BETWEEN, args)
224225

225-
def nearest_neighbor(self, query_vector, element_count: int) -> QueryCondition:
226+
def nearest_neighbor(self, query_vector, element_count: int) -> PropertyQueryCondition:
226227
args = {'query_vector': query_vector, 'element_count': element_count}
227-
return QueryCondition(self._id, _QueryConditionOp.NEAREST_NEIGHBOR, args)
228+
return PropertyQueryCondition(self._id, PropertyQueryConditionOp.NEAREST_NEIGHBOR, args)
228229

229-
def contains_key_value(self, key: str, value: str, case_sensitive: bool = True) -> QueryCondition:
230+
def contains_key_value(self, key: str, value: str, case_sensitive: bool = True) -> PropertyQueryCondition:
230231
args = {'key': key, 'value': value, 'case_sensitive': case_sensitive}
231-
return QueryCondition(self._id, _QueryConditionOp.CONTAINS_KEY_VALUE, args)
232+
return PropertyQueryCondition(self._id, PropertyQueryConditionOp.CONTAINS_KEY_VALUE, args)
232233

233234

234235
# ID property (primary key)

objectbox/query.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ def find_ids(self) -> List[int]:
4646
c_id_array_p = obx_query_find_ids(self._c_query)
4747
try:
4848
c_id_array: OBX_id_array = c_id_array_p.contents
49+
if c_id_array.count == 0:
50+
return []
4951
ids = ctypes.cast(c_id_array.ids, ctypes.POINTER(obx_id * c_id_array.count))
5052
return list(ids.contents)
5153
finally:

0 commit comments

Comments
 (0)