Skip to content

Commit 15b3266

Browse files
committed
Merge branch '36-flex-key-equals-value-cond' into 'dev'
Resolve "Bubble up flex "key equals value" condition" #36 Closes #36 See merge request objectbox/objectbox-python!25
2 parents e7abcd7 + 16c282f commit 15b3266

File tree

7 files changed

+132
-61
lines changed

7 files changed

+132
-61
lines changed

objectbox/condition.py

Lines changed: 12 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class _QueryConditionOp(Enum):
1515
LTE = 9
1616
BETWEEN = 10
1717
NEAREST_NEIGHBOR = 11
18+
CONTAINS_KEY_VALUE = 12
1819

1920

2021
class QueryCondition:
@@ -43,7 +44,8 @@ def _get_op_map(self):
4344
_QueryConditionOp.LT: self._apply_lt,
4445
_QueryConditionOp.LTE: self._apply_lte,
4546
_QueryConditionOp.BETWEEN: self._apply_between,
46-
_QueryConditionOp.NEAREST_NEIGHBOR: self._apply_nearest_neighbor
47+
_QueryConditionOp.NEAREST_NEIGHBOR: self._apply_nearest_neighbor,
48+
_QueryConditionOp.CONTAINS_KEY_VALUE: self._contains_key_value
4749
# ... new query condition here ... :)
4850
}
4951

@@ -57,9 +59,6 @@ def _apply_eq(self, qb: 'QueryBuilder'):
5759
else:
5860
raise Exception(f"Unsupported type for 'EQ': {type(value)}")
5961

60-
if self._alias is not None:
61-
qb.alias(self._alias)
62-
6362
def _apply_not_eq(self, qb: 'QueryBuilder'):
6463
value = self._args['value']
6564
case_sensitive = self._args['case_sensitive']
@@ -70,9 +69,6 @@ def _apply_not_eq(self, qb: 'QueryBuilder'):
7069
else:
7170
raise Exception(f"Unsupported type for 'NOT_EQ': {type(value)}")
7271

73-
if self._alias is not None:
74-
qb.alias(self._alias)
75-
7672
def _apply_contains(self, qb: 'QueryBuilder'):
7773
value = self._args['value']
7874
case_sensitive = self._args['case_sensitive']
@@ -81,9 +77,6 @@ def _apply_contains(self, qb: 'QueryBuilder'):
8177
else:
8278
raise Exception(f"Unsupported type for 'CONTAINS': {type(value)}")
8379

84-
if self._alias is not None:
85-
qb.alias(self._alias)
86-
8780
def _apply_starts_with(self, qb: 'QueryBuilder'):
8881
value = self._args['value']
8982
case_sensitive = self._args['case_sensitive']
@@ -92,9 +85,6 @@ def _apply_starts_with(self, qb: 'QueryBuilder'):
9285
else:
9386
raise Exception(f"Unsupported type for 'STARTS_WITH': {type(value)}")
9487

95-
if self._alias is not None:
96-
qb.alias(self._alias)
97-
9888
def _apply_ends_with(self, qb: 'QueryBuilder'):
9989
value = self._args['value']
10090
case_sensitive = self._args['case_sensitive']
@@ -103,9 +93,6 @@ def _apply_ends_with(self, qb: 'QueryBuilder'):
10393
else:
10494
raise Exception(f"Unsupported type for 'ENDS_WITH': {type(value)}")
10595

106-
if self._alias is not None:
107-
qb.alias(self._alias)
108-
10996
def _apply_gt(self, qb: 'QueryBuilder'):
11097
value = self._args['value']
11198
case_sensitive = self._args['case_sensitive']
@@ -116,9 +103,6 @@ def _apply_gt(self, qb: 'QueryBuilder'):
116103
else:
117104
raise Exception(f"Unsupported type for 'GT': {type(value)}")
118105

119-
if self._alias is not None:
120-
qb.alias(self._alias)
121-
122106
def _apply_gte(self, qb: 'QueryBuilder'):
123107
value = self._args['value']
124108
case_sensitive = self._args['case_sensitive']
@@ -129,9 +113,6 @@ def _apply_gte(self, qb: 'QueryBuilder'):
129113
else:
130114
raise Exception(f"Unsupported type for 'GTE': {type(value)}")
131115

132-
if self._alias is not None:
133-
qb.alias(self._alias)
134-
135116
def _apply_lt(self, qb: 'QueryCondition'):
136117
value = self._args['value']
137118
case_sensitive = self._args['case_sensitive']
@@ -142,9 +123,6 @@ def _apply_lt(self, qb: 'QueryCondition'):
142123
else:
143124
raise Exception("Unsupported type for 'LT': " + str(type(value)))
144125

145-
if self._alias is not None:
146-
qb.alias(self._alias)
147-
148126
def _apply_lte(self, qb: 'QueryBuilder'):
149127
value = self._args['value']
150128
case_sensitive = self._args['case_sensitive']
@@ -155,9 +133,6 @@ def _apply_lte(self, qb: 'QueryBuilder'):
155133
else:
156134
raise Exception(f"Unsupported type for 'LTE': {type(value)}")
157135

158-
if self._alias is not None:
159-
qb.alias(self._alias)
160-
161136
def _apply_between(self, qb: 'QueryBuilder'):
162137
a = self._args['a']
163138
b = self._args['b']
@@ -166,9 +141,6 @@ def _apply_between(self, qb: 'QueryBuilder'):
166141
else:
167142
raise Exception(f"Unsupported type for 'BETWEEN': {type(a)}")
168143

169-
if self._alias is not None:
170-
qb.alias(self._alias)
171-
172144
def _apply_nearest_neighbor(self, qb: 'QueryBuilder'):
173145
query_vector = self._args['query_vector']
174146
element_count = self._args['element_count']
@@ -184,8 +156,15 @@ def _apply_nearest_neighbor(self, qb: 'QueryBuilder'):
184156
else:
185157
raise Exception(f"Unsupported type for 'NEAREST_NEIGHBOR': {type(query_vector)}")
186158

187-
if self._alias is not None:
188-
qb.alias(self._alias)
159+
def _contains_key_value(self, qb: 'QueryBuilder'):
160+
key = self._args['key']
161+
value = self._args['value']
162+
case_sensitive = self._args['case_sensitive']
163+
qb.contains_key_value(self._property_id, key, value, case_sensitive)
189164

190165
def apply(self, qb: 'QueryBuilder'):
166+
""" Applies the stored condition to the supplied query builder. """
191167
self._get_op_map()[self._op](qb)
168+
169+
if self._alias is not None:
170+
qb.alias(self._alias)

objectbox/model/properties.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,14 @@ def between(self, a, b) -> QueryCondition:
200200
args = {'a': a, 'b': b}
201201
return QueryCondition(self._id, _QueryConditionOp.BETWEEN, args)
202202

203-
def nearest_neighbor(self, query_vector, element_count: int):
203+
def nearest_neighbor(self, query_vector, element_count: int) -> QueryCondition:
204204
args = {'query_vector': query_vector, 'element_count': element_count}
205205
return QueryCondition(self._id, _QueryConditionOp.NEAREST_NEIGHBOR, args)
206206

207+
def contains_key_value(self, key: str, value: str, case_sensitive: bool = True) -> QueryCondition:
208+
args = {'key': key, 'value': value, 'case_sensitive': case_sensitive}
209+
return QueryCondition(self._id, _QueryConditionOp.CONTAINS_KEY_VALUE, args)
210+
207211

208212
# ID property (primary key)
209213
class Id(Property):

objectbox/query_builder.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,18 @@ def nearest_neighbors_f32(self, prop: Union[int, str, Property], query_vector: U
117117
cond = obx_qb_nearest_neighbors_f32(self._c_builder, prop_id, c_query_vector, element_count)
118118
return cond
119119

120+
def contains_key_value(self, prop: Union[int, str, Property], key: str, value: str,
121+
case_sensitive: bool = True) -> obx_qb_cond:
122+
""" Checks whether the given Flex property, interpreted as a dictionary and indexed at key, has a value
123+
corresponding to the given value.
124+
125+
:param case_sensitive:
126+
If false, ignore case when matching value
127+
"""
128+
prop_id = self._entity.get_property_id(prop)
129+
cond = obx_qb_contains_key_value_string(self._c_builder, prop_id, c_str(key), c_str(value), case_sensitive)
130+
return cond
131+
120132
def any(self, conditions: List[obx_qb_cond]) -> obx_qb_cond:
121133
c_conditions = c_array(conditions, obx_qb_cond)
122134
cond = obx_qb_any(self._c_builder, c_conditions, len(conditions))

tests/common.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def load_empty_test_datetime(name: str = "") -> objectbox.ObjectBox:
3131

3232
def load_empty_test_flex(name: str = "") -> objectbox.ObjectBox:
3333
model = objectbox.Model()
34-
model.entity(TestEntityFlex, last_property_id=IdUid(3, 3003))
34+
model.entity(TestEntityFlex, last_property_id=IdUid(2, 3002))
3535
model.last_entity_id = IdUid(3, 3)
3636

3737
db_name = test_dir if len(name) == 0 else test_dir + "/" + name
@@ -51,7 +51,7 @@ def create_test_objectbox(db_name: Optional[str] = None, clear_db: bool = True)
5151
model = objectbox.Model()
5252
model.entity(TestEntity, last_property_id=IdUid(27, 1027))
5353
model.entity(TestEntityDatetime, last_property_id=IdUid(4, 2004))
54-
model.entity(TestEntityFlex, last_property_id=IdUid(3, 3003))
54+
model.entity(TestEntityFlex, last_property_id=IdUid(2, 3002))
5555
model.entity(VectorEntity, last_property_id=IdUid(3, 4003))
5656
model.last_entity_id = IdUid(4, 4)
5757
model.last_index_id = IdUid(3, 40001)

tests/model.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@ class TestEntityDatetime:
5050
@Entity(id=3, uid=3)
5151
class TestEntityFlex:
5252
id = Id(id=1, uid=3001)
53-
flex_dict = Property(Dict[str, Any], type=PropertyType.flex, id=2, uid=3002)
54-
flex_int = Property(int, type=PropertyType.flex, id=3, uid=3003)
53+
flex = Property(Any, type=PropertyType.flex, id=2, uid=3002)
5554

5655

5756
@Entity(id=4, uid=4)

tests/test_box.py

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def test_box_basics():
7777

7878
# remove
7979
box.remove(object)
80-
80+
8181
# remove should return success
8282
success = box.remove(1)
8383
assert success == True
@@ -229,22 +229,33 @@ def test_put_get(object: TestEntity, box: objectbox.Box, property):
229229
ob.close()
230230

231231

232-
def test_flex_dict():
233-
ob = load_empty_test_flex()
234-
box = objectbox.Box(ob, TestEntityFlex)
235-
object = TestEntityFlex()
232+
def test_flex_values():
233+
ob = create_test_objectbox()
236234

237-
# Put an empty object
238-
id = box.put(object)
239-
assert id == object.id
240-
read = box.get(object.id)
241-
assert read.flex_dict == None
242-
assert read.flex_int == None
235+
box = objectbox.Box(ob, TestEntityFlex)
243236

244-
object.flex_dict = {"a": 1, "b": 2}
245-
object.flex_int = 25
246-
id = box.put(object)
247-
assert id == object.id
248-
read = box.get(object.id)
249-
assert read.flex_dict == object.flex_dict
250-
assert read.flex_int == object.flex_int
237+
# Test empty object
238+
obj_id = box.put(TestEntityFlex())
239+
read_obj = box.get(obj_id)
240+
assert read_obj.flex is None
241+
242+
# Test int
243+
obj_id = box.put(TestEntityFlex(flex=23))
244+
read_obj = box.get(obj_id)
245+
assert read_obj.flex == 23
246+
247+
# Test string
248+
obj_id = box.put(TestEntityFlex(flex="hello"))
249+
read_obj = box.get(obj_id)
250+
assert read_obj.flex == "hello"
251+
252+
# Test mixed list
253+
obj_id = box.put(TestEntityFlex(flex=[4, 5, 1, "foo", 23, "bar"]))
254+
read_obj = box.get(obj_id)
255+
assert read_obj.flex == [4, 5, 1, "foo", 23, "bar"]
256+
257+
# Test dictionary
258+
dict_ = {"a": 1, "b": {"list": [1, 2, 3], "int": 5}}
259+
obj_id = box.put(TestEntityFlex(flex=dict_))
260+
read_obj = box.get(obj_id)
261+
assert read_obj.flex == dict_

tests/test_query.py

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,54 @@ def test_basics():
107107
ob.close()
108108

109109

110+
def test_flex_contains_key_value():
111+
ob = create_test_objectbox()
112+
113+
box = objectbox.Box(ob, TestEntityFlex)
114+
box.put(TestEntityFlex(flex={"k1": "String", "k2": 2, "k3": "string"}))
115+
box.put(TestEntityFlex(flex={"k1": "strinG", "k2": 3, "k3": 10, "k4": [1, "foo", 3]}))
116+
box.put(TestEntityFlex(flex={"k1": "buzz", "k2": 3, "k3": [2, 3], "k4": {"k1": "a", "k2": "inner text"}}))
117+
box.put(TestEntityFlex(flex={"n1": "string", "n2": -7, "n3": [-10, 10], "n4": [4, 4, 4]}))
118+
box.put(TestEntityFlex(flex={"n1": "Apple", "n2": 3, "n3": [2, 3, 5], "n4": {"n1": [1, 2, "bar"]}}))
119+
120+
assert box.count() == 5
121+
122+
# Search case-sensitive = False
123+
flex: Property = TestEntityFlex.get_property("flex")
124+
query = box.query(flex.contains_key_value("k1", "string", False)).build()
125+
results = query.find()
126+
assert len(results) == 2
127+
assert results[0].flex["k1"] == "String"
128+
assert results[0].flex["k2"] == 2
129+
assert results[0].flex["k3"] == "string"
130+
assert results[1].flex["k1"] == "strinG"
131+
assert results[1].flex["k2"] == 3
132+
assert results[1].flex["k3"] == 10
133+
assert results[1].flex["k4"] == [1, "foo", 3]
134+
135+
# Search case-sensitive = True
136+
flex: Property = TestEntityFlex.get_property("flex")
137+
query = box.query(flex.contains_key_value("n1", "string", True)).build()
138+
results = query.find()
139+
assert len(results) == 1
140+
assert results[0].flex["n1"] == "string"
141+
assert results[0].flex["n2"] == -7
142+
assert results[0].flex["n3"] == [-10, 10]
143+
assert results[0].flex["n4"] == [4, 4, 4]
144+
145+
# TODO Search using nested key (not supported yet)
146+
147+
# No match (key)
148+
flex: Property = TestEntityFlex.get_property("flex")
149+
query = box.query(flex.contains_key_value("missing key", "string", True)).build()
150+
assert len(query.find()) == 0
151+
152+
# No match (value)
153+
flex: Property = TestEntityFlex.get_property("flex")
154+
query = box.query(flex.contains_key_value("k1", "missing value", True)).build()
155+
assert len(query.find()) == 0
156+
157+
110158
def test_offset_limit():
111159
ob = load_empty_test_objectbox()
112160

@@ -265,20 +313,23 @@ def test_set_parameter_alias():
265313
box_vector.put(VectorEntity(name="Object 4", vector=[4, 4]))
266314
box_vector.put(VectorEntity(name="Object 5", vector=[5, 5]))
267315

268-
str_prop: Property = TestEntity.properties[1]
269-
qb = box.query(str_prop.equals("Foo").alias("foo_filter"))
316+
str_prop: Property = TestEntity.get_property("str")
317+
int32_prop: Property = TestEntity.get_property("int32")
318+
int64_prop: Property = TestEntity.get_property("int64")
270319

320+
# Test set parameter alias on string
321+
qb = box.query(str_prop.equals("Foo").alias("foo_filter"))
271322
query = qb.build()
323+
272324
assert query.find()[0].str == "Foo"
273325
assert query.count() == 1
274326

275327
query.set_parameter_alias_string("foo_filter", "FooBar")
276-
277328
assert query.find()[0].str == "FooBar"
278329
assert query.count() == 1
279330

280-
int_prop: Property = TestEntity.properties[3]
281-
qb = box.query(int_prop.greater_than(5).alias("greater_than_filter"))
331+
# Test set parameter alias on int64
332+
qb = box.query(int64_prop.greater_than(5).alias("greater_than_filter"))
282333

283334
query = qb.build()
284335
assert query.count() == 1
@@ -288,6 +339,21 @@ def test_set_parameter_alias():
288339

289340
assert query.count() == 2
290341

342+
# Test set parameter alias on string/int32
343+
qb = box.query(str_prop.equals("Foo").alias("str condition"))
344+
int32_prop.greater_than(700).alias("int32 condition").apply(qb)
345+
query = qb.build()
346+
347+
assert query.count() == 1
348+
assert query.find()[0].str == "Foo"
349+
350+
query.set_parameter_alias_string("str condition", "FooBar") # FooBar int32 isn't higher than 700 (49)
351+
assert query.count() == 0
352+
353+
query.set_parameter_alias_int("int32 condition", 40)
354+
assert query.find()[0].str == "FooBar"
355+
356+
# Test set parameter alias on vector
291357
vector_prop: Property = VectorEntity.get_property("vector")
292358

293359
query = box_vector.query(vector_prop.nearest_neighbor([3.4, 3.4], 3).alias("nearest_neighbour_filter")).build()

0 commit comments

Comments
 (0)