Skip to content

Commit e598276

Browse files
committed
feat(firestore): add BSON read deserialization support
1 parent 136d4cc commit e598276

6 files changed

Lines changed: 105 additions & 54 deletions

File tree

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

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
import google
4545
from google.cloud import exceptions # type: ignore
4646
from google.cloud.firestore_v1 import transforms, types
47-
from google.cloud.firestore_v1.bson import _BSONType
47+
from google.cloud.firestore_v1.bson import _BSON_DECODERS, _BSONType
4848
from google.cloud.firestore_v1.field_path import FieldPath, parse_field_path
4949
from google.cloud.firestore_v1.types import common, document, write
5050
from google.cloud.firestore_v1.types.write import DocumentTransform
@@ -350,7 +350,18 @@ def reference_value_to_document(reference_value, client) -> Any:
350350
def decode_value(
351351
value, client
352352
) -> Union[
353-
None, bool, int, float, list, datetime.datetime, str, bytes, dict, GeoPoint, Vector
353+
None,
354+
bool,
355+
int,
356+
float,
357+
list,
358+
datetime.datetime,
359+
str,
360+
bytes,
361+
dict,
362+
GeoPoint,
363+
Vector,
364+
_BSONType,
354365
]:
355366
"""Converts a Firestore protobuf ``Value`` to a native Python value.
356367
@@ -402,7 +413,20 @@ def decode_value(
402413
raise ValueError("Unknown ``value_type``", value_type)
403414

404415

405-
def decode_dict(value_fields, client) -> Union[dict, Vector]:
416+
def _decode_bson_dict(data: dict) -> Optional[_BSONType]:
417+
"""Decode a single-key wire map dictionary if registered."""
418+
if len(data) == 1:
419+
key, val = next(iter(data.items()))
420+
decoder = _BSON_DECODERS.get(key)
421+
if decoder is not None:
422+
try:
423+
return decoder(val)
424+
except Exception:
425+
pass
426+
return None
427+
428+
429+
def decode_dict(value_fields, client) -> Union[dict, Vector, _BSONType]:
406430
"""Converts a protobuf map of Firestore ``Value``-s.
407431
408432
Args:
@@ -412,9 +436,9 @@ def decode_dict(value_fields, client) -> Union[dict, Vector]:
412436
A client that has a document factory.
413437
414438
Returns:
415-
Dict[str, Union[NoneType, bool, int, float, datetime.datetime, \
416-
str, bytes, dict, ~google.cloud.Firestore.GeoPoint]]: A dictionary
417-
of native Python values converted from the ``value_fields``.
439+
Union[dict, ~google.cloud.firestore_v1.vector.Vector, \
440+
~google.cloud.firestore_v1.bson._BSONType]: A dictionary of native \
441+
Python values, Vector, or BSON object converted from ``value_fields``.
418442
"""
419443
value_fields_pb = getattr(value_fields, "_pb", value_fields)
420444
res = {key: decode_value(value, client) for key, value in value_fields_pb.items()}
@@ -425,7 +449,7 @@ def decode_dict(value_fields, client) -> Union[dict, Vector]:
425449
values = cast(Sequence[float], res["value"])
426450
return Vector(values)
427451

428-
return res
452+
return _decode_bson_dict(res) or res
429453

430454

431455
def get_doc_id(document_pb, expected_prefix) -> str:

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

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
import abc
2828
import decimal
2929
import re
30-
from typing import Any, Dict, Union
30+
from typing import Any, Callable, Dict, Union
3131

3232
__all__ = [
3333
"BSONObjectId",
@@ -531,7 +531,23 @@ def __eq__(self, other: Any) -> bool:
531531
return NotImplemented
532532

533533
def __hash__(self) -> int:
534-
normalized_str = (
535-
"NAN" if self._value.upper() == "NAN" else self._value
536-
)
534+
normalized_str = "NAN" if self._value.upper() == "NAN" else self._value
537535
return hash((type(self), normalized_str))
536+
537+
538+
_BSON_DECODERS: Dict[str, Callable[[Any], Any]] = {
539+
"__oid__": BSONObjectId,
540+
"__min__": lambda _: BSONMinKey(),
541+
"__max__": lambda _: BSONMaxKey(),
542+
"__int__": BSONInt32,
543+
"__decimal128__": BSONDecimal128,
544+
"__binary__": lambda v: BSONBinary(v[1:], subtype=v[0])
545+
if isinstance(v, (bytes, bytearray)) and v
546+
else None,
547+
"__request_timestamp__": lambda v: BSONTimestamp(v["seconds"], v["increment"])
548+
if isinstance(v, dict) and "seconds" in v and "increment" in v
549+
else None,
550+
"__regex__": lambda v: BSONRegex(v["pattern"], v.get("options", ""))
551+
if isinstance(v, dict) and "pattern" in v
552+
else None,
553+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
from google.cloud.firestore_v1.async_transaction import AsyncTransaction
4545
from google.cloud.firestore_v1.base_client import BaseClient
4646
from google.cloud.firestore_v1.base_document import BaseDocumentReference
47+
from google.cloud.firestore_v1.bson import _BSONType
4748
from google.cloud.firestore_v1.client import Client
4849
from google.cloud.firestore_v1.pipeline import Pipeline
4950
from google.cloud.firestore_v1.pipeline_expressions import Constant
@@ -138,7 +139,7 @@ def __eq__(self, other: object) -> bool:
138139
return NotImplemented
139140
return (self._ref == other._ref) and (self._fields_pb == other._fields_pb)
140141

141-
def data(self) -> dict | "Vector" | None:
142+
def data(self) -> dict | "Vector" | "_BSONType" | None:
142143
"""
143144
Retrieves all fields in the result.
144145

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

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,9 +1285,9 @@ def test_unicode_doc(client, cleanup, database):
12851285

12861286

12871287
@pytest.mark.parametrize("database", [FIRESTORE_ENTERPRISE_DB], indirect=True)
1288-
def test_bson_document_writes(client, cleanup, database):
1289-
"""Test write operations for BSON types on Enterprise DB."""
1290-
collection_id = "bson_type_writes_" + UNIQUE_RESOURCE_ID
1288+
def test_bson_document_read_and_write(client, cleanup, database):
1289+
"""Test read and write operations for BSON types on Enterprise DB."""
1290+
collection_id = "bson_type_read_write_" + UNIQUE_RESOURCE_ID
12911291
doc_ref = client.collection(collection_id).document("bson_doc")
12921292
cleanup(doc_ref.delete)
12931293

@@ -1307,24 +1307,14 @@ def test_bson_document_writes(client, cleanup, database):
13071307
snapshot = doc_ref.get()
13081308
assert snapshot.exists
13091309
assert snapshot.to_dict() == {
1310-
"user_id": {"__oid__": "507f191e810c19729de860ea"},
1311-
"min_key": {"__min__": None},
1312-
"max_key": {"__max__": None},
1313-
"int32_val": {"__int__": 42},
1314-
"binary_val_sub128": {"__binary__": b"\x80world"},
1315-
"timestamp_val": {
1316-
"__request_timestamp__": {
1317-
"seconds": 1700000000,
1318-
"increment": 1,
1319-
}
1320-
},
1321-
"regex_val": {
1322-
"__regex__": {
1323-
"pattern": "^hello.*$",
1324-
"options": "i",
1325-
}
1326-
},
1327-
"decimal128_val": {"__decimal128__": "123.45"},
1310+
"user_id": BSONObjectId("507f191e810c19729de860ea"),
1311+
"min_key": BSONMinKey(),
1312+
"max_key": BSONMaxKey(),
1313+
"int32_val": BSONInt32(42),
1314+
"binary_val_sub128": BSONBinary(b"world", subtype=128),
1315+
"timestamp_val": BSONTimestamp(1700000000, 1),
1316+
"regex_val": BSONRegex("^hello.*$", options="i"),
1317+
"decimal128_val": BSONDecimal128("123.45"),
13281318
}
13291319

13301320

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

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,9 +1258,9 @@ async def test_list_collections_with_read_time(client, cleanup, database):
12581258

12591259
@pytest.mark.asyncio
12601260
@pytest.mark.parametrize("database", [FIRESTORE_ENTERPRISE_DB], indirect=True)
1261-
async def test_async_bson_document_writes(client, cleanup, database):
1262-
"""Test async write operations for BSON types on Enterprise DB."""
1263-
collection_id = "async_bson_type_writes_" + UNIQUE_RESOURCE_ID
1261+
async def test_async_bson_document_read_and_write(client, cleanup, database):
1262+
"""Test async read and write operations for BSON types on Enterprise DB."""
1263+
collection_id = "async_bson_type_read_write_" + UNIQUE_RESOURCE_ID
12641264
doc_ref = client.collection(collection_id).document("bson_doc")
12651265
cleanup(doc_ref.delete)
12661266

@@ -1280,24 +1280,14 @@ async def test_async_bson_document_writes(client, cleanup, database):
12801280
snapshot = await doc_ref.get()
12811281
assert snapshot.exists
12821282
assert snapshot.to_dict() == {
1283-
"user_id": {"__oid__": "507f191e810c19729de860ea"},
1284-
"min_key": {"__min__": None},
1285-
"max_key": {"__max__": None},
1286-
"int32_val": {"__int__": 42},
1287-
"binary_val_sub128": {"__binary__": b"\x80world"},
1288-
"timestamp_val": {
1289-
"__request_timestamp__": {
1290-
"seconds": 1700000000,
1291-
"increment": 1,
1292-
}
1293-
},
1294-
"regex_val": {
1295-
"__regex__": {
1296-
"pattern": "^hello.*$",
1297-
"options": "i",
1298-
}
1299-
},
1300-
"decimal128_val": {"__decimal128__": "123.45"},
1283+
"user_id": BSONObjectId("507f191e810c19729de860ea"),
1284+
"min_key": BSONMinKey(),
1285+
"max_key": BSONMaxKey(),
1286+
"int32_val": BSONInt32(42),
1287+
"binary_val_sub128": BSONBinary(b"world", subtype=128),
1288+
"timestamp_val": BSONTimestamp(1700000000, 1),
1289+
"regex_val": BSONRegex("^hello.*$", options="i"),
1290+
"decimal128_val": BSONDecimal128("123.45"),
13011291
}
13021292

13031293

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,36 @@ def test_decode_dict_w_many_types():
706706
assert decode_dict(value_fields, mock.sentinel.client) == expected
707707

708708

709+
def test_decode_dict_w_bson_types():
710+
from google.cloud.firestore_v1._helpers import decode_dict, encode_dict
711+
from google.cloud.firestore_v1.bson import (
712+
BSONBinary,
713+
BSONDecimal128,
714+
BSONInt32,
715+
BSONMaxKey,
716+
BSONMinKey,
717+
BSONObjectId,
718+
BSONRegex,
719+
BSONTimestamp,
720+
)
721+
722+
original_dict = {
723+
"oid": BSONObjectId("507f191e810c19729de860ea"),
724+
"min_k": BSONMinKey(),
725+
"max_k": BSONMaxKey(),
726+
"int32_v": BSONInt32(42),
727+
"bin_sub0": b"hello",
728+
"bin_sub128": BSONBinary(b"world", subtype=128),
729+
"ts_v": BSONTimestamp(1700000000, 1),
730+
"regex_v": BSONRegex("^hello.*$", options="i"),
731+
"dec_v": BSONDecimal128("123.45"),
732+
}
733+
734+
pb_fields = encode_dict(original_dict)
735+
decoded = decode_dict(pb_fields, mock.sentinel.client)
736+
assert decoded == original_dict
737+
738+
709739
def _dummy_ref_string(collection_id):
710740
from google.cloud.firestore_v1.base_client import DEFAULT_DATABASE
711741

0 commit comments

Comments
 (0)