Skip to content

Commit 4e0532e

Browse files
committed
Fail fast for malformed geo_distance expressions
- Replace permissive fallbacks in _build_geo_filter_expression with ValueError (was: '@alias >= 0' which silently broadened results) - Refactor _process_geo_distance_select to validate arguments strictly: - Require at least 2 args - First arg must be a column - Second arg must be POINT(lon, lat) with literal values - Unit arg (if present) must be literal - Fix _add_condition to fail fast when geo_distance() is detected but POINT coordinates can't be parsed (was: fallback to regular Condition) - Fix _add_between_condition with same fail-fast behavior This ensures malformed geo_distance predicates raise clear errors instead of silently producing incorrect queries.
1 parent 70f0338 commit 4e0532e

2 files changed

Lines changed: 64 additions & 35 deletions

File tree

sql_redis/parser.py

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -337,44 +337,59 @@ def _process_vector_distance(
337337
def _process_geo_distance_select(
338338
self, expression, result: ParsedQuery, alias: str | None
339339
) -> None:
340-
"""Process geo_distance() in SELECT clause for FT.AGGREGATE APPLY."""
340+
"""Process geo_distance() in SELECT clause for FT.AGGREGATE APPLY.
341+
342+
Expected signature: geo_distance(field, POINT(lon, lat)[, unit])
343+
Raises ValueError for malformed usage rather than silently ignoring.
344+
"""
341345
func_args = expression.expressions
342346
if not func_args:
343-
return
344-
345-
field_name = None
346-
geo_lon = None
347-
geo_lat = None
348-
geo_unit = "m" # Default to meters for geodistance()
347+
raise ValueError(
348+
"geo_distance() requires at least 2 arguments: "
349+
"geo_distance(field, POINT(lon, lat)[, unit])"
350+
)
349351

350-
# First arg: field name
351-
if isinstance(func_args[0], exp.Column):
352-
field_name = func_args[0].name
352+
# First arg: field name must be a column
353+
if not isinstance(func_args[0], exp.Column):
354+
raise ValueError("geo_distance() first argument must be a column reference")
355+
field_name = func_args[0].name
353356

354-
# Second arg: POINT(lon, lat) - matches Redis's native format
355-
if len(func_args) >= 2 and isinstance(func_args[1], exp.Anonymous):
356-
point_func = func_args[1]
357-
if point_func.name.upper() == "POINT" and len(point_func.expressions) >= 2:
358-
# POINT(lon, lat) - no swap needed, matches Redis
359-
geo_lon = self._extract_literal_value(point_func.expressions[0])
360-
geo_lat = self._extract_literal_value(point_func.expressions[1])
357+
# Second arg: POINT(lon, lat) required
358+
if len(func_args) < 2:
359+
raise ValueError(
360+
"geo_distance() requires a POINT(lon, lat) second argument"
361+
)
362+
if not isinstance(func_args[1], exp.Anonymous):
363+
raise ValueError("geo_distance() second argument must be POINT(lon, lat)")
364+
point_func = func_args[1]
365+
if point_func.name.upper() != "POINT" or len(point_func.expressions) < 2:
366+
raise ValueError("geo_distance() second argument must be POINT(lon, lat)")
367+
368+
# Extract literal lon/lat values
369+
geo_lon = self._extract_literal_value(point_func.expressions[0])
370+
geo_lat = self._extract_literal_value(point_func.expressions[1])
371+
if geo_lon is None or geo_lat is None:
372+
raise ValueError(
373+
"geo_distance() POINT(lon, lat) arguments must be literal values"
374+
)
361375

362376
# Third arg (optional): unit
377+
geo_unit = "m" # Default to meters
363378
if len(func_args) >= 3:
364379
unit_val = self._extract_literal_value(func_args[2])
365-
if unit_val:
366-
geo_unit = self._validate_geo_unit(unit_val)
380+
if unit_val is None:
381+
raise ValueError("geo_distance() unit argument must be a literal value")
382+
geo_unit = self._validate_geo_unit(unit_val)
367383

368-
if field_name and geo_lon is not None and geo_lat is not None:
369-
result.geo_distance_selects.append(
370-
GeoDistanceSelect(
371-
field=field_name,
372-
lon=float(geo_lon),
373-
lat=float(geo_lat),
374-
alias=alias or "geo_distance",
375-
unit=geo_unit,
376-
)
384+
result.geo_distance_selects.append(
385+
GeoDistanceSelect(
386+
field=field_name,
387+
lon=float(geo_lon),
388+
lat=float(geo_lat),
389+
alias=alias or "geo_distance",
390+
unit=geo_unit,
377391
)
392+
)
378393

379394
def _process_where_clause(
380395
self, expression, result: ParsedQuery, negated: bool = False
@@ -463,7 +478,12 @@ def _add_condition(
463478
value = int(value) if "." not in str(value) else float(value)
464479

465480
if field_name is not None:
466-
if is_geo_distance and geo_lon is not None and geo_lat is not None:
481+
if is_geo_distance:
482+
# Fail fast if POINT(lon, lat) coordinates couldn't be parsed
483+
if geo_lon is None or geo_lat is None:
484+
raise ValueError(
485+
"geo_distance() requires POINT(lon, lat) with literal values"
486+
)
467487
# Negated geo_distance is not supported; fail clearly
468488
if negated:
469489
raise ValueError(
@@ -546,7 +566,12 @@ def _add_between_condition(
546566
high_val = self._extract_literal_value(high)
547567

548568
if field_name is not None:
549-
if is_geo_distance and geo_lon is not None and geo_lat is not None:
569+
if is_geo_distance:
570+
# Fail fast if POINT(lon, lat) coordinates couldn't be parsed
571+
if geo_lon is None or geo_lat is None:
572+
raise ValueError(
573+
"geo_distance() BETWEEN requires POINT(lon, lat) with literal values"
574+
)
550575
# Negation is not supported for geo_distance BETWEEN; fail clearly
551576
if negated:
552577
raise ValueError(

sql_redis/translator.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -407,14 +407,18 @@ def _build_geo_filter_expression(
407407
high_m = self._convert_to_meters(geo_cond.radius[1], geo_cond.unit)
408408
return f"@{alias} >= {low_m} && @{alias} <= {high_m}"
409409
else:
410-
# Fallback - shouldn't happen
411-
return f"@{alias} >= 0"
410+
# Internal inconsistency: BETWEEN requires (low, high) tuple
411+
raise ValueError(
412+
f"Invalid geo radius for BETWEEN operator: {geo_cond.radius!r}"
413+
)
412414

413415
# Convert radius to meters if needed (geodistance() returns meters)
414-
# At this point, radius is guaranteed to be a float (BETWEEN case handled above)
416+
# At this point, radius should be a float (BETWEEN case handled above)
415417
if isinstance(geo_cond.radius, tuple):
416-
# Shouldn't reach here, but handle gracefully
417-
return f"@{alias} >= 0"
418+
# Internal inconsistency: tuple radius outside BETWEEN context
419+
raise ValueError(
420+
f"Unexpected tuple geo radius outside BETWEEN: {geo_cond.radius!r}"
421+
)
418422
radius_m = self._convert_to_meters(geo_cond.radius, geo_cond.unit)
419423

420424
if geo_cond.operator == ">":

0 commit comments

Comments
 (0)