Skip to content

Commit 29d971e

Browse files
committed
fix: parenthesize multi-word LIKE patterns, raise on invalid fulltext/fuzzy signatures
- LIKE '%gaming laptop%' now generates @title:(*gaming laptop*) to prevent token leaking across fields in Dialect 2 - fulltext(title) and fuzzy(title) with < 2 args now raise ValueError instead of silently dropping the predicate - Update existing test expectation for insufficient args - Add 3 new tests (379 total)
1 parent 8963c6a commit 29d971e

4 files changed

Lines changed: 33 additions & 5 deletions

File tree

sql_redis/parser.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,12 @@ def _add_function_condition(
980980
func_name = expression.name.upper()
981981
args = expression.expressions
982982

983+
if func_name in ("FULLTEXT", "FUZZY") and len(args) < 2:
984+
raise ValueError(
985+
f"{func_name.lower()}() requires at least 2 arguments: "
986+
f"{func_name.lower()}(field, value), got {len(args)}."
987+
)
988+
983989
if func_name == "FULLTEXT" and len(args) >= 2:
984990
field_name = args[0].name if isinstance(args[0], exp.Column) else None
985991
value = self._extract_literal_value(args[1])

sql_redis/query_builder.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ def build_text_condition(
125125
parts = value.split("%")
126126
escaped_parts = [self._escape_fulltext_term(p) for p in parts]
127127
search_value = "*".join(escaped_parts)
128+
# If the non-wildcard portion contains spaces, wrap in parens
129+
# so all tokens stay scoped to the field (e.g. '%gaming laptop%'
130+
# → *gaming laptop* needs grouping to avoid token leaking).
131+
non_wildcard = value.strip("%")
132+
if " " in non_wildcard:
133+
search_value = f"({search_value})"
128134
elif operator == "FUZZY":
129135
# Escape special chars before wrapping with % markers
130136
escaped = self._escape_fulltext_term(value)

tests/test_query_builder.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,12 @@ def test_suffix_negated(self):
427427
result = builder.build_text_condition("title", "LIKE", "%phone", negated=True)
428428
assert result == "-@title:*phone"
429429

430+
def test_infix_multiword_grouped(self):
431+
"""LIKE '%multi word%' groups tokens in parentheses."""
432+
builder = QueryBuilder()
433+
result = builder.build_text_condition("title", "LIKE", "%gaming laptop%")
434+
assert result == "@title:(*gaming laptop*)"
435+
430436

431437
class TestQueryBuilderORInText:
432438
"""Tests for OR/union within text field searches."""

tests/test_sql_parser.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -622,12 +622,10 @@ def test_parse_fulltext_non_column_first_arg(self):
622622
assert len(result.conditions) == 0
623623

624624
def test_parse_fulltext_insufficient_args(self):
625-
"""Parse fulltext with insufficient arguments."""
625+
"""Parse fulltext with insufficient arguments raises ValueError."""
626626
parser = SQLParser()
627-
result = parser.parse("SELECT * FROM products WHERE fulltext(title)")
628-
629-
# Only 1 arg, needs >= 2 - condition skipped
630-
assert len(result.conditions) == 0
627+
with pytest.raises(ValueError, match="requires at least 2 arguments"):
628+
parser.parse("SELECT * FROM products WHERE fulltext(title)")
631629

632630
def test_parse_geo_distance_no_args(self):
633631
"""Parse geo_distance with no arguments in comparison."""
@@ -897,3 +895,15 @@ def test_fulltext_invalid_inorder_raises(self):
897895
parser.parse(
898896
"SELECT * FROM idx WHERE fulltext(title, 'hello world', 0, 'yes')"
899897
)
898+
899+
def test_fulltext_no_value_raises(self):
900+
"""fulltext() with only field arg raises ValueError."""
901+
parser = SQLParser()
902+
with pytest.raises(ValueError, match="requires at least 2 arguments"):
903+
parser.parse("SELECT * FROM idx WHERE fulltext(title)")
904+
905+
def test_fuzzy_no_value_raises(self):
906+
"""fuzzy() with only field arg raises ValueError."""
907+
parser = SQLParser()
908+
with pytest.raises(ValueError, match="requires at least 2 arguments"):
909+
parser.parse("SELECT * FROM idx WHERE fuzzy(title)")

0 commit comments

Comments
 (0)