Skip to content

Commit 5be825c

Browse files
committed
fixing filter tests and removing unit tests covered by integration tests
1 parent 67d52b1 commit 5be825c

2 files changed

Lines changed: 44 additions & 55 deletions

File tree

integrations/supabase/src/haystack_integrations/document_stores/supabase/groonga_document_store.py

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -217,24 +217,46 @@ def _apply_filters(query: Any, filters: dict[str, Any]) -> Any:
217217
query = SupabaseGroongaDocumentStore._apply_filters(query, cond)
218218
return query
219219

220-
if op in ("OR", "NOT"):
221-
neg_map = {"==": "neq", "!=": "eq", ">": "lte", ">=": "lt", "<": "gte", "<=": "gt"}
220+
if op == "OR":
222221
pg_op_map = {"==": "eq", "!=": "neq", ">": "gt", ">=": "gte", "<": "lt", "<=": "lte"}
223-
op_map = neg_map if op == "NOT" else pg_op_map
224222
parts = []
225223
for cond in conditions:
226224
if "field" not in cond:
227-
msg = f"Nested logical operators inside {op} are not supported."
225+
msg = "Nested logical operators inside OR are not supported."
226+
raise FilterError(msg)
227+
cond_field = cond.get("field", "")
228+
cond_op = cond.get("operator", "")
229+
cond_value = cond.get("value")
230+
if cond_op not in pg_op_map:
231+
msg = f"Operator '{cond_op}' inside OR filter is not supported."
232+
raise FilterError(msg)
233+
# Use text accessor (->>): PostgREST OR strings don't support JSONB (->) expressions.
234+
col = f"meta->>{cond_field[len('meta.'):]}" if cond_field.startswith("meta.") else cond_field
235+
norm = SupabaseGroongaDocumentStore._normalize_value(cond_value)
236+
parts.append(f"{col}.{pg_op_map[cond_op]}.{norm}")
237+
return query.or_(",".join(parts))
238+
239+
if op == "NOT":
240+
# NOT(A AND B) = NOT_A OR NOT_B, with null-inclusive semantics.
241+
# Use text accessor: PostgREST OR strings don't support JSONB (->) expressions.
242+
neg_map = {"==": "neq", "!=": "eq", ">": "lte", ">=": "lt", "<": "gte", "<=": "gt"}
243+
parts = []
244+
for cond in conditions:
245+
if "field" not in cond:
246+
msg = "Nested logical operators inside NOT are not supported."
228247
raise FilterError(msg)
229248
cond_field = cond.get("field", "")
230249
cond_op = cond.get("operator", "")
231250
cond_value = cond.get("value")
232-
if cond_op not in op_map:
233-
msg = f"Operator '{cond_op}' inside {op} filter is not supported."
251+
if cond_op not in neg_map:
252+
msg = f"Operator '{cond_op}' inside NOT filter is not supported."
234253
raise FilterError(msg)
235-
col = SupabaseGroongaDocumentStore._meta_col(cond_field, cond_value)
254+
col = f"meta->>{cond_field[len('meta.'):]}" if cond_field.startswith("meta.") else cond_field
236255
norm = SupabaseGroongaDocumentStore._normalize_value(cond_value)
237-
parts.append(f"{col}.{op_map[cond_op]}.{norm}")
256+
parts.append(f"{col}.{neg_map[cond_op]}.{norm}")
257+
if cond_op == "==" and cond_field.startswith("meta."):
258+
# NOT(field==value) also covers docs where the field is absent (SQL NULL semantics)
259+
parts.append(f"{col}.is.null")
238260
return query.or_(",".join(parts))
239261

240262
msg = f"Filter operator '{op}' is not supported. Supported logical operators: AND, OR, NOT."
@@ -262,7 +284,13 @@ def _apply_condition(query: Any, condition: dict[str, Any]) -> Any:
262284
return query.is_(col, "null") if norm is None else query.eq(col, norm)
263285

264286
if op == "!=":
265-
return query.not_.is_(col, "null") if norm is None else query.neq(col, norm)
287+
if norm is None:
288+
return query.not_.is_(col, "null")
289+
if field.startswith("meta."):
290+
# SQL: NULL != value returns NULL (not TRUE), so include docs where the field is absent.
291+
key = field[len("meta."):]
292+
return query.or_(f"{col}.neq.{norm},meta->>{key}.is.null")
293+
return query.neq(col, norm)
266294

267295
if op in (">", ">=", "<", "<="):
268296
if isinstance(value, list):
@@ -294,6 +322,12 @@ def _apply_condition(query: Any, condition: dict[str, Any]) -> Any:
294322
if not isinstance(value, list):
295323
msg = "Filter operator 'not in' requires a list value."
296324
raise FilterError(msg)
325+
if field.startswith("meta."):
326+
# SQL: NULL NOT IN (...) returns NULL, so include docs where the field is absent.
327+
key = field[len("meta."):]
328+
non_none = [v for v in value if v is not None]
329+
vals = ",".join(str(v) for v in non_none)
330+
return query.or_(f"{col}.not.in.({vals}),meta->>{key}.is.null")
297331
return query.not_.in_(col, value)
298332

299333
return query

integrations/supabase/tests/test_groonga_document_store.py

Lines changed: 1 addition & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -134,51 +134,6 @@ def test_write_documents_fail_on_duplicate(self, groonga_store, mock_supabase_cl
134134
with pytest.raises(DuplicateDocumentError):
135135
groonga_store.write_documents([Document(content="duplicate doc")], policy=DuplicatePolicy.FAIL)
136136

137-
def test_delete_by_filter(self, groonga_store, mock_supabase_client):
138-
mock_table = mock_supabase_client.table.return_value
139-
mock_table.select.return_value.execute.return_value = MagicMock(
140-
data=[{"id": "1", "content": "doc one", "meta": {"lang": "en"}, "score": None}]
141-
)
142-
mock_table.delete.return_value.in_.return_value.execute.return_value = MagicMock(data=[])
143-
144-
deleted = groonga_store.delete_by_filter(
145-
filters={"conditions": [{"field": "meta.lang", "operator": "==", "value": "en"}]}
146-
)
147-
assert deleted == 1
148-
149-
def test_delete_by_filter_no_matches(self, groonga_store, mock_supabase_client):
150-
mock_supabase_client.table.return_value.select.return_value.execute.return_value = MagicMock(data=[])
151-
152-
deleted = groonga_store.delete_by_filter(
153-
filters={"conditions": [{"field": "meta.lang", "operator": "==", "value": "fr"}]}
154-
)
155-
assert deleted == 0
156-
157-
def test_update_by_filter(self, groonga_store, mock_supabase_client):
158-
mock_table = mock_supabase_client.table.return_value
159-
mock_table.select.return_value.execute.return_value = MagicMock(
160-
data=[{"id": "1", "content": "doc one", "meta": {"lang": "en"}, "score": None}]
161-
)
162-
mock_table.upsert.return_value.execute.return_value = MagicMock(data=[{}])
163-
164-
updated = groonga_store.update_by_filter(
165-
filters={"conditions": [{"field": "meta.lang", "operator": "==", "value": "en"}]},
166-
meta={"reviewed": True},
167-
)
168-
assert updated == 1
169-
mock_table.upsert.assert_called_once()
170-
upserted_row = mock_table.upsert.call_args[0][0]
171-
assert upserted_row["meta"] == {"lang": "en", "reviewed": True}
172-
173-
def test_update_by_filter_no_matches(self, groonga_store, mock_supabase_client):
174-
mock_supabase_client.table.return_value.select.return_value.execute.return_value = MagicMock(data=[])
175-
176-
updated = groonga_store.update_by_filter(
177-
filters={"conditions": [{"field": "meta.lang", "operator": "==", "value": "fr"}]},
178-
meta={"reviewed": True},
179-
)
180-
assert updated == 0
181-
182137
def test_delete_all_documents(self, groonga_store, mock_supabase_client):
183138
mock_table = mock_supabase_client.table.return_value
184139
mock_table.delete.return_value.neq.return_value.execute.return_value = MagicMock(data=[])
@@ -214,7 +169,7 @@ def test_filter_documents_with_filters(self, groonga_store, mock_supabase_client
214169
mock_table.select.return_value.eq.return_value.execute.return_value = MagicMock(
215170
data=[{"id": "1", "content": "Python is great", "meta": {"language": "en"}, "score": None}]
216171
)
217-
filters = {"conditions": [{"field": "meta.language", "operator": "==", "value": "en"}]}
172+
filters = {"operator": "AND", "conditions": [{"field": "meta.language", "operator": "==", "value": "en"}]}
218173
docs = groonga_store.filter_documents(filters=filters)
219174
assert len(docs) == 1
220175

0 commit comments

Comments
 (0)