Skip to content

Commit f7aaa62

Browse files
committed
fix(firestore): disallow float in BSONDecimal128 and validate input strings
Disallow passing float to BSONDecimal128 to prevent binary floating-point precision loss and edge cases, aligning with PyMongo's Decimal128 behavior. Validate string and numeric inputs via decimal.Decimal in __init__ to reject invalid numeric strings.
1 parent 5440ed6 commit f7aaa62

2 files changed

Lines changed: 27 additions & 20 deletions

File tree

‎packages/google-cloud-firestore/google/cloud/firestore_v1/bson.py‎

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -413,12 +413,12 @@ class BSONDecimal128(_BSONType):
413413
"""Represents a BSON 128-bit Decimal container for Firestore.
414414
415415
Args:
416-
value (Union[str, int, float, decimal.Decimal, BSONDecimal128]):
417-
The decimal value as a string, integer, float, decimal.Decimal,
416+
value (Union[str, int, decimal.Decimal, BSONDecimal128]):
417+
The decimal value as a string, integer, decimal.Decimal,
418418
or BSONDecimal128 instance.
419419
420420
Raises:
421-
TypeError: If value is a boolean or unsupported type.
421+
TypeError: If value is a boolean or an unsupported type.
422422
ValueError: If value cannot be parsed as a valid decimal number.
423423
424424
Example:
@@ -433,18 +433,26 @@ class BSONDecimal128(_BSONType):
433433

434434
def __init__(
435435
self,
436-
value: Union[str, int, float, decimal.Decimal, "BSONDecimal128"],
436+
value: Union[str, int, decimal.Decimal, "BSONDecimal128"],
437437
):
438438
if isinstance(value, BSONDecimal128):
439439
self._value: str = value._value
440-
elif isinstance(value, (str, int, float, decimal.Decimal)) and not isinstance(
440+
elif isinstance(value, (str, int, decimal.Decimal)) and not isinstance(
441441
value, bool
442442
):
443+
try:
444+
# Validate the value parses as a valid decimal number
445+
decimal.Decimal(value)
446+
except decimal.InvalidOperation as exc:
447+
raise ValueError(f"Cannot convert {value!r} to Decimal: {exc}") from exc
443448
self._value = str(value)
444-
else:
449+
elif isinstance(value, float):
445450
raise TypeError(
446-
"BSONDecimal128 value must be a Decimal, str, int, or float."
451+
"BSONDecimal128 does not accept float values due to potential precision loss. "
452+
"Convert the float to a str or decimal.Decimal first."
447453
)
454+
else:
455+
raise TypeError("BSONDecimal128 value must be a Decimal, str, or int.")
448456

449457
@property
450458
def value(self) -> str:

‎packages/google-cloud-firestore/tests/unit/v1/test_bson.py‎

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -496,14 +496,11 @@ def test_bson_decimal128_valid():
496496
dec2 = BSONDecimal128(42)
497497
assert dec2.value == "42"
498498

499-
dec3 = BSONDecimal128(1.5)
500-
assert dec3.value == "1.5"
499+
dec3 = BSONDecimal128(decimal.Decimal("99.99"))
500+
assert dec3.value == "99.99"
501501

502-
dec4 = BSONDecimal128(decimal.Decimal("99.99"))
503-
assert dec4.value == "99.99"
504-
505-
dec5 = BSONDecimal128(dec1)
506-
assert dec5.value == "123.45"
502+
dec4 = BSONDecimal128(dec1)
503+
assert dec4.value == "123.45"
507504

508505

509506
def test_bson_decimal128_float_and_int():
@@ -523,9 +520,6 @@ def test_bson_decimal128_float_and_int():
523520
"-NaN",
524521
"sNaN",
525522
"-sNaN",
526-
float("inf"),
527-
float("-inf"),
528-
float("nan"),
529523
],
530524
)
531525
def test_bson_decimal128_special_values(special_val):
@@ -545,9 +539,14 @@ def test_bson_decimal128_special_values(special_val):
545539
@pytest.mark.parametrize(
546540
"val_input, exc_type, match_msg",
547541
[
548-
(True, TypeError, "value must be a Decimal, str, int, or float"),
549-
(False, TypeError, "value must be a Decimal, str, int, or float"),
550-
([1, 2], TypeError, "value must be a Decimal, str, int, or float"),
542+
(True, TypeError, "value must be a Decimal, str, or int"),
543+
(False, TypeError, "value must be a Decimal, str, or int"),
544+
([1, 2], TypeError, "value must be a Decimal, str, or int"),
545+
(1.5, TypeError, "does not accept float values"),
546+
(float("inf"), TypeError, "does not accept float values"),
547+
(float("nan"), TypeError, "does not accept float values"),
548+
("not-a-number", ValueError, "Cannot convert 'not-a-number' to Decimal"),
549+
("12.34.56", ValueError, "Cannot convert '12.34.56' to Decimal"),
551550
],
552551
)
553552
def test_bson_decimal128_invalid_inputs(val_input, exc_type, match_msg):

0 commit comments

Comments
 (0)