Skip to content

Commit 39c55c9

Browse files
sjrldavidsbatista
andauthored
fix(alloydb): parenthesize not in clause so it does not escape an AND filter (#3655)
Co-authored-by: David S. Batista <dsbatista@gmail.com>
1 parent 90ca9a8 commit 39c55c9

2 files changed

Lines changed: 28 additions & 1 deletion

File tree

integrations/alloydb/src/haystack_integrations/document_stores/alloydb/filters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ def _not_in(field: Composable, value: Any) -> tuple[Composed, list]:
229229
if not isinstance(value, list):
230230
msg = f"{field}'s value must be a list when using 'not in' comparator in AlloyDB"
231231
raise FilterError(msg)
232-
return SQL("{} IS NULL OR {} != ALL(%s)").format(field, field), [value]
232+
return SQL("({} IS NULL OR {} != ALL(%s))").format(field, field), [value]
233233

234234

235235
def _in(field: Composable, value: Any) -> tuple[Composed, list]:

integrations/alloydb/tests/test_filters.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,33 @@ def test_comparison_condition_unknown_operator():
138138
_parse_comparison_condition(condition)
139139

140140

141+
@pytest.mark.parametrize("operator", ["like", "not like"])
142+
def test_comparison_condition_like_operator_requires_str_value(operator):
143+
# AlloyDB-specific: LIKE / NOT LIKE require a string operand.
144+
condition = {"field": "meta.name", "operator": operator, "value": 123}
145+
with pytest.raises(FilterError, match="must be a str when using"):
146+
_parse_comparison_condition(condition)
147+
148+
149+
def test_logical_condition_not_in_is_parenthesized_when_and_combined():
150+
# `not in` renders an internal `OR` (`IS NULL OR != ALL`). Without surrounding
151+
# parentheses that `OR` escapes an enclosing `AND` (AND binds tighter than OR in
152+
# SQL), so an AND-combined `not in` would wrongly match documents.
153+
condition = {
154+
"operator": "AND",
155+
"conditions": [
156+
{"field": "meta.number", "operator": "==", "value": 5},
157+
{"field": "meta.author", "operator": "not in", "value": ["John", "Jane"]},
158+
],
159+
}
160+
query, values = _parse_logical_condition(condition)
161+
assert isinstance(query, Composed)
162+
163+
expected_sql = "((meta->>'number')::integer = %s AND (meta->>'author' IS NULL OR meta->>'author' != ALL(%s)))"
164+
assert _render(query) == expected_sql
165+
assert values == [5, [["John", "Jane"]]]
166+
167+
141168
def test_logical_condition_missing_operator():
142169
condition = {"conditions": []}
143170
with pytest.raises(FilterError):

0 commit comments

Comments
 (0)