Skip to content

Commit 0f09d53

Browse files
committed
feat(firestore): add BSONInt32 support
1 parent edd355d commit 0f09d53

7 files changed

Lines changed: 154 additions & 2 deletions

File tree

.librarian/generator-input/client-post-processing/firestore-integration.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ replacements:
7070
from google.cloud.firestore_v1.base_query import And, FieldFilter, Or
7171
from google.cloud.firestore_v1.batch import WriteBatch
7272
from google.cloud.firestore_v1.bson import (
73+
BSONInt32,
7374
BSONMaxKey,
7475
BSONMinKey,
7576
BSONObjectId,
@@ -175,6 +176,7 @@ replacements:
175176
"async_transactional",
176177
"AsyncTransaction",
177178
"AsyncWriteBatch",
179+
"BSONInt32",
178180
"BSONMaxKey",
179181
"BSONMinKey",
180182
"BSONObjectId",
@@ -250,6 +252,7 @@ replacements:
250252
AsyncQuery,
251253
AsyncTransaction,
252254
AsyncWriteBatch,
255+
BSONInt32,
253256
BSONMaxKey,
254257
BSONMinKey,
255258
BSONObjectId,
@@ -310,6 +313,7 @@ replacements:
310313
"async_transactional",
311314
"AsyncTransaction",
312315
"AsyncWriteBatch",
316+
"BSONInt32",
313317
"BSONMaxKey",
314318
"BSONMinKey",
315319
"BSONObjectId",

packages/google-cloud-firestore/google/cloud/firestore/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
AsyncQuery,
3636
AsyncTransaction,
3737
AsyncWriteBatch,
38+
BSONInt32,
3839
BSONMaxKey,
3940
BSONMinKey,
4041
BSONObjectId,
@@ -95,6 +96,7 @@
9596
"async_transactional",
9697
"AsyncTransaction",
9798
"AsyncWriteBatch",
99+
"BSONInt32",
98100
"BSONMaxKey",
99101
"BSONMinKey",
100102
"BSONObjectId",

packages/google-cloud-firestore/google/cloud/firestore_v1/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
from google.cloud.firestore_v1.base_query import And, FieldFilter, Or
4848
from google.cloud.firestore_v1.batch import WriteBatch
4949
from google.cloud.firestore_v1.bson import (
50+
BSONInt32,
5051
BSONMaxKey,
5152
BSONMinKey,
5253
BSONObjectId,
@@ -152,6 +153,7 @@
152153
"async_transactional",
153154
"AsyncTransaction",
154155
"AsyncWriteBatch",
156+
"BSONInt32",
155157
"BSONMaxKey",
156158
"BSONMinKey",
157159
"BSONObjectId",

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

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"BSONObjectId",
3333
"BSONMinKey",
3434
"BSONMaxKey",
35+
"BSONInt32",
3536
]
3637

3738
_OBJECT_ID_BYTES_LEN = 12
@@ -156,3 +157,60 @@ def __eq__(self, other: Any) -> bool:
156157

157158
def __hash__(self) -> int:
158159
return hash(type(self))
160+
161+
162+
class BSONInt32(_BSONType):
163+
"""Represents a 32-bit signed integer value container for Firestore BSON.
164+
165+
Args:
166+
value (int): A 32-bit signed integer value.
167+
168+
Raises:
169+
TypeError: If value is not an integer or is a boolean.
170+
ValueError: If value is outside the 32-bit signed range (-2147483648 to 2147483647).
171+
172+
Example:
173+
>>> int_val = BSONInt32(42)
174+
>>> int_val.value
175+
42
176+
"""
177+
178+
__slots__ = ("_value",)
179+
180+
_MIN_VALUE: int = -(1 << 31)
181+
_MAX_VALUE: int = (1 << 31) - 1
182+
183+
def __init__(self, value: int):
184+
if isinstance(value, bool) or not isinstance(value, int):
185+
raise TypeError("BSONInt32 requires an int.")
186+
if not (self._MIN_VALUE <= value <= self._MAX_VALUE):
187+
raise ValueError(
188+
f"BSONInt32 value must be between {self._MIN_VALUE} and {self._MAX_VALUE}."
189+
)
190+
self._value: int = value
191+
192+
@property
193+
def value(self) -> int:
194+
"""int: The 32-bit signed integer value."""
195+
return self._value
196+
197+
def _to_map_value(self) -> Dict[str, int]:
198+
"""Returns map dictionary representation for wire serialization."""
199+
return {"__int__": self._value}
200+
201+
def __repr__(self) -> str:
202+
return f"BSONInt32({self._value})"
203+
204+
def __str__(self) -> str:
205+
return str(self._value)
206+
207+
def __int__(self) -> int:
208+
return self._value
209+
210+
def __eq__(self, other: Any) -> bool:
211+
if isinstance(other, BSONInt32):
212+
return self._value == other._value
213+
return NotImplemented
214+
215+
def __hash__(self) -> int:
216+
return hash((type(self), self._value))

packages/google-cloud-firestore/tests/system/test_system.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,12 @@
4848
from google.cloud import firestore_v1 as firestore
4949
from google.cloud.firestore_v1.base_query import And, FieldFilter, Or
5050
from google.cloud.firestore_v1.base_vector_query import DistanceMeasure
51-
from google.cloud.firestore_v1.bson import BSONMaxKey, BSONMinKey, BSONObjectId
51+
from google.cloud.firestore_v1.bson import (
52+
BSONInt32,
53+
BSONMaxKey,
54+
BSONMinKey,
55+
BSONObjectId,
56+
)
5257
from google.cloud.firestore_v1.vector import Vector
5358

5459

@@ -1286,6 +1291,7 @@ def test_bson_document_writes(client, cleanup, database):
12861291
"user_id": BSONObjectId("507f191e810c19729de860ea"),
12871292
"min_key": BSONMinKey(),
12881293
"max_key": BSONMaxKey(),
1294+
"int32_val": BSONInt32(42),
12891295
}
12901296

12911297
doc_ref.set(bson_payload)
@@ -1296,6 +1302,7 @@ def test_bson_document_writes(client, cleanup, database):
12961302
"user_id": {"__oid__": "507f191e810c19729de860ea"},
12971303
"min_key": {"__min__": None},
12981304
"max_key": {"__max__": None},
1305+
"int32_val": {"__int__": 42},
12991306
}
13001307

13011308

packages/google-cloud-firestore/tests/system/test_system_async.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,12 @@
5151
from google.cloud import firestore_v1 as firestore
5252
from google.cloud.firestore_v1.base_query import And, FieldFilter, Or
5353
from google.cloud.firestore_v1.base_vector_query import DistanceMeasure
54-
from google.cloud.firestore_v1.bson import BSONMaxKey, BSONMinKey, BSONObjectId
54+
from google.cloud.firestore_v1.bson import (
55+
BSONInt32,
56+
BSONMaxKey,
57+
BSONMinKey,
58+
BSONObjectId,
59+
)
5560
from google.cloud.firestore_v1.query_profile import (
5661
ExecutionStats,
5762
ExplainMetrics,
@@ -1259,6 +1264,7 @@ async def test_async_bson_document_writes(client, cleanup, database):
12591264
"user_id": BSONObjectId("507f191e810c19729de860ea"),
12601265
"min_key": BSONMinKey(),
12611266
"max_key": BSONMaxKey(),
1267+
"int32_val": BSONInt32(42),
12621268
}
12631269

12641270
await doc_ref.set(bson_payload)
@@ -1269,6 +1275,7 @@ async def test_async_bson_document_writes(client, cleanup, database):
12691275
"user_id": {"__oid__": "507f191e810c19729de860ea"},
12701276
"min_key": {"__min__": None},
12711277
"max_key": {"__max__": None},
1278+
"int32_val": {"__int__": 42},
12721279
}
12731280

12741281

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

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import pytest
2222

2323
from google.cloud.firestore_v1.bson import (
24+
BSONInt32,
2425
BSONMaxKey,
2526
BSONMinKey,
2627
BSONObjectId,
@@ -195,3 +196,74 @@ def test_bson_maxkey_copy():
195196
def test_bson_maxkey_pickle():
196197
key = BSONMaxKey()
197198
assert pickle.loads(pickle.dumps(key)) == key
199+
200+
201+
def test_bson_int32_valid():
202+
val = BSONInt32(42)
203+
assert val.value == 42
204+
assert int(val) == 42
205+
assert str(val) == "42"
206+
assert repr(val) == "BSONInt32(42)"
207+
assert val._to_map_value() == {"__int__": 42}
208+
209+
210+
def test_bson_int32_boundaries():
211+
min_val = BSONInt32(-2147483648)
212+
max_val = BSONInt32(2147483647)
213+
assert min_val.value == -2147483648
214+
assert max_val.value == 2147483647
215+
216+
217+
@pytest.mark.parametrize(
218+
"invalid_input, exc_type, match_msg",
219+
[
220+
(2147483648, ValueError, "between -2147483648 and 2147483647"),
221+
(-2147483649, ValueError, "between -2147483648 and 2147483647"),
222+
("42", TypeError, "requires an int"),
223+
(42.0, TypeError, "requires an int"),
224+
(True, TypeError, "requires an int"),
225+
(False, TypeError, "requires an int"),
226+
(None, TypeError, "requires an int"),
227+
],
228+
)
229+
def test_bson_int32_invalid(invalid_input, exc_type, match_msg):
230+
with pytest.raises(exc_type, match=match_msg):
231+
BSONInt32(invalid_input)
232+
233+
234+
def test_bson_int32_equality():
235+
val1 = BSONInt32(42)
236+
val2 = BSONInt32(42)
237+
val3 = BSONInt32(100)
238+
239+
assert val1 == val2
240+
assert val1 != val3
241+
assert val1 != 42 # BSONInt32 is not equal to plain int
242+
243+
class CooperativeOther:
244+
def __eq__(self, other):
245+
return True
246+
247+
assert val1 == CooperativeOther()
248+
249+
250+
def test_bson_int32_hash_and_dict_key():
251+
val1 = BSONInt32(42)
252+
val2 = BSONInt32(42)
253+
254+
assert hash(val1) == hash(val2)
255+
assert hash(val1) != hash(42)
256+
lookup = {val1: "success"}
257+
assert lookup[val2] == "success"
258+
assert len({val1, val2}) == 1
259+
260+
261+
def test_bson_int32_copy():
262+
val = BSONInt32(42)
263+
assert copy.copy(val) == val
264+
assert copy.deepcopy(val) == val
265+
266+
267+
def test_bson_int32_pickle():
268+
val = BSONInt32(42)
269+
assert pickle.loads(pickle.dumps(val)) == val

0 commit comments

Comments
 (0)