mirror of
https://github.com/qdrant/qdrant-client.git
synced 2026-09-21 13:37:55 -05:00
fix: reject 0 raised to a negative exponent in formula PowExpression (#1429)
* fix: reject 0 raised to a negative exponent in formula PowExpression evaluate_expression's PowExpression branch let base == 0 through the base >= 0 check regardless of the exponent's sign, then called math.pow(0, exponent). For a negative exponent this is a pole (division by zero) and math.pow raises a raw ValueError: math domain error, instead of the library's own raise_non_finite_error message that every other non-finite case in this function produces. Split the base >= 0 check into base > 0 (always fine) and base == 0 (fine only for a non-negative exponent), leaving the existing base < 0 integer-exponent branch untouched. 0 raised to a non-negative exponent still returns the expected value (0**0 == 1, 0**3 == 0). Adds test_pow_expression to qdrant_client/local/tests/test_formula.py covering the previously-passing cases plus the new negative-exponent regression, alongside the existing negative-base non-integer-exponent case. * fix: match core's non-finite handling in formula pow --------- Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
This commit is contained in:
committed by
George Panchuk
co-authored by
George Panchuk
parent
05441fd670
commit
18d46069c3
@@ -107,13 +107,14 @@ def evaluate_expression(
|
||||
)
|
||||
|
||||
try:
|
||||
# `float()` is needed because payload values and formula
|
||||
# defaults can be ints, and `int.is_integer()` only exists since Python 3.12.
|
||||
# the condition is inside the try-except because too large integers can't be converted to floats.
|
||||
if base >= 0 or (base != 0 and float(exponent).is_integer()):
|
||||
return math.pow(base, exponent)
|
||||
except OverflowError:
|
||||
# OverflowError: the result, or a too large integer operand, doesn't fit in a float.
|
||||
# ValueError: undefined, e.g. a negative base with a non-integer exponent.
|
||||
result = math.pow(base, exponent)
|
||||
except (OverflowError, ValueError):
|
||||
pass
|
||||
else:
|
||||
if math.isfinite(result):
|
||||
return result
|
||||
|
||||
raise_non_finite_error(f"{base}^{exponent}")
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import math
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from qdrant_client.http import models
|
||||
from qdrant_client.hybrid.formula import (
|
||||
evaluate_expression,
|
||||
evaluate_variable,
|
||||
parse_variable,
|
||||
try_extract_payload_value,
|
||||
@@ -50,6 +53,46 @@ def test_evaluate_variable_rejects_negative_score_index() -> None:
|
||||
evaluate_variable("$score[-9]", 1, scores, {}, {})
|
||||
|
||||
|
||||
def _pow(
|
||||
base: models.Expression, exponent: models.Expression, payload: models.Payload | None = None
|
||||
) -> float:
|
||||
expression = models.PowExpression(pow=models.PowParams(base=base, exponent=exponent))
|
||||
return evaluate_expression(expression, 1, [], payload or {}, {}, {})
|
||||
|
||||
|
||||
def test_pow_expression() -> None:
|
||||
assert _pow(2.0, 3.0) == 8.0
|
||||
assert _pow(-2.0, 3.0) == -8.0
|
||||
assert _pow(-2.0, 4.0) == 16.0
|
||||
assert _pow(0.0, 0.0) == 1.0
|
||||
assert _pow(0.0, 3.0) == 0.0
|
||||
|
||||
# qdrant core computes `base.powf(exponent)` and rejects anything non-finite,
|
||||
# so every case below must surface as the same non-finite error
|
||||
|
||||
# a negative base with a non-integer exponent is undefined
|
||||
with pytest.raises(ValueError, match="non-finite"):
|
||||
_pow(-2.0, 2.5)
|
||||
|
||||
# 0 raised to a negative exponent is a pole (division by zero)
|
||||
with pytest.raises(ValueError, match="non-finite"):
|
||||
_pow(0.0, -1.0)
|
||||
with pytest.raises(ValueError, match="non-finite"):
|
||||
_pow(0.0, -2.5)
|
||||
|
||||
# the result overflows a float
|
||||
with pytest.raises(ValueError, match="non-finite"):
|
||||
_pow(10.0, 400.0)
|
||||
|
||||
# an infinite operand, reachable through a sum/product that overflowed
|
||||
with pytest.raises(ValueError, match="non-finite"):
|
||||
_pow(2.0, math.inf)
|
||||
|
||||
# an integer payload value too large to be converted to a float
|
||||
with pytest.raises(ValueError, match="non-finite"):
|
||||
_pow(-2.0, "exponent", payload={"exponent": 10**400})
|
||||
|
||||
|
||||
def test_try_extract_payload_value() -> None:
|
||||
for payload_value, expected in [(1.2, 1.2), ([1.2], 1.2), ([1.2, 2.3], [1.2, 2.3])]:
|
||||
empty_defaults: dict[str, Any] = {}
|
||||
|
||||
Reference in New Issue
Block a user