Skip to content

Commit a9eea83

Browse files
committed
address PR #17 review comments
- Fix FULLTEXT operator naming (MATCH → FULLTEXT) in query_builder and tests - Resolve score alias once per result set to prevent column name flip-flop - Add fuzzy_level range validation (1-3) in parser - Fix import formatting (isort/black) in translator.py and analyzer.py
1 parent 17434f1 commit a9eea83

4 files changed

Lines changed: 28 additions & 20 deletions

File tree

sql_redis/executor.py

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -228,20 +228,20 @@ def execute(self, sql: str, *, params: dict | None = None) -> QueryResult:
228228
elif with_scores:
229229
# WITHSCORES format: [count, key1, score1, [fields1], key2, score2, [fields2], ...]
230230
# Stride of 3: key, score, field_list
231-
# Resolve alias per-row: FT.SEARCH may omit missing attributes,
232-
# so different rows can have different field sets. We must
233-
# check each row individually to avoid overwriting a real field
234-
# with the score value.
231+
# Resolve alias once from the first row so every row uses the
232+
# same column name (consistent output schema).
233+
resolved_alias: str | None = None
235234
for i in range(1, len(raw_result) - 2, 3):
236235
score = raw_result[i + 1]
237236
row_data = raw_result[i + 2]
238237
row = dict(zip(row_data[::2], row_data[1::2]))
239-
row_score_alias = self._resolve_score_alias(
240-
translated.score_alias,
241-
translated.args,
242-
first_row_fields=set(row.keys()),
243-
)
244-
row[row_score_alias] = score
238+
if resolved_alias is None:
239+
resolved_alias = self._resolve_score_alias(
240+
translated.score_alias,
241+
translated.args,
242+
first_row_fields=set(row.keys()),
243+
)
244+
row[resolved_alias] = score
245245
rows.append(row)
246246
else:
247247
# Standard format: [count, key1, [fields1], key2, [fields2], ...]
@@ -345,17 +345,20 @@ async def execute(self, sql: str, *, params: dict | None = None) -> QueryResult:
345345
rows.append(row)
346346
elif with_scores:
347347
# WITHSCORES format: [count, key1, score1, [fields1], ...]
348-
# Resolve alias per-row to avoid field collision on later rows.
348+
# Resolve alias once from the first row so every row uses the
349+
# same column name (consistent output schema).
350+
resolved_alias: str | None = None
349351
for i in range(1, len(raw_result) - 2, 3):
350352
score = raw_result[i + 1]
351353
row_data = raw_result[i + 2]
352354
row = dict(zip(row_data[::2], row_data[1::2]))
353-
row_score_alias = self._resolve_score_alias(
354-
translated.score_alias,
355-
translated.args,
356-
first_row_fields=set(row.keys()),
357-
)
358-
row[row_score_alias] = score
355+
if resolved_alias is None:
356+
resolved_alias = self._resolve_score_alias(
357+
translated.score_alias,
358+
translated.args,
359+
first_row_fields=set(row.keys()),
360+
)
361+
row[resolved_alias] = score
359362
rows.append(row)
360363
else:
361364
# Standard format: [count, key1, [fields1], key2, [fields2], ...]

sql_redis/parser.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,11 @@ def _add_function_condition(
10741074
f"FUZZY level argument must be an integer (got {level_val})"
10751075
)
10761076
fuzzy_level = int(level_val)
1077+
if fuzzy_level not in (1, 2, 3):
1078+
raise ValueError(
1079+
f"FUZZY level must be 1, 2, or 3 (got {fuzzy_level}). "
1080+
"RediSearch supports a maximum Levenshtein distance of 3."
1081+
)
10771082

10781083
if field_name is None:
10791084
raise ValueError(

sql_redis/query_builder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ def build_text_condition(
180180
or_parts.append(self._escape_fulltext_term(token))
181181
search_value = f"({'|'.join(or_parts)})"
182182
elif " " in value:
183-
# FULLTEXT/MATCH with multi-word: tokenized search with stopword filtering.
183+
# FULLTEXT with multi-word: tokenized search with stopword filtering.
184184
# Each term is escaped to prevent accidental operator injection, but a
185185
# leading ~ (optional-term modifier) is preserved as an intentional
186186
# RediSearch operator.

tests/test_query_builder.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ def test_simple_text_query(self):
324324
"""Build simple text search query."""
325325
builder = QueryBuilder()
326326
result = builder.build_query_string(
327-
text_conditions=[("title", "MATCH", "laptop")],
327+
text_conditions=[("title", "FULLTEXT", "laptop")],
328328
field_types={"title": "TEXT"},
329329
)
330330

@@ -334,7 +334,7 @@ def test_combined_query(self):
334334
"""Build combined text + numeric + tag query."""
335335
builder = QueryBuilder()
336336
result = builder.build_query_string(
337-
text_conditions=[("title", "MATCH", "laptop")],
337+
text_conditions=[("title", "FULLTEXT", "laptop")],
338338
numeric_conditions=[("price", "<", 1000)],
339339
tag_conditions=[("category", "=", "electronics")],
340340
field_types={"title": "TEXT", "price": "NUMERIC", "category": "TAG"},

0 commit comments

Comments
 (0)