Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fcffa27
feat(firestore): add BSONRegex support
ohmayr Sep 16, 2026
d133864
refactor(firestore): address review feedback for BSONRegex options va…
ohmayr Sep 21, 2026
a614d3f
refactor(firestore): defer BSONRegex options validation to backend
ohmayr Sep 22, 2026
7dad0c3
test(firestore): add system tests for BSONRegex with invalid options
ohmayr Sep 24, 2026
9e7e5f5
feat(firestore): add BSONDecimal128 support
ohmayr Sep 16, 2026
a52bb0a
refactor(firestore): address review feedback for BSONDecimal128
ohmayr Sep 21, 2026
96bcd32
fix(firestore): handle signaling NaN strings in BSONDecimal128.__float__
ohmayr Sep 21, 2026
f513edc
fix(firestore): disallow float in BSONDecimal128 and validate input s…
ohmayr Sep 23, 2026
b7ac973
feat(firestore): add BSON read deserialization support
ohmayr Sep 16, 2026
5bab625
refactor(firestore): address review feedback for BSON read deserializ…
ohmayr Sep 21, 2026
b3443ff
refactor(firestore): restore precise return types and docstrings on d…
ohmayr Sep 21, 2026
f4bba9b
refactor(firestore): align decode_dict and PipelineResult.data return…
ohmayr Sep 21, 2026
f6327c1
fix(firestore): restore required client parameter and format comprehe…
ohmayr Sep 22, 2026
b065d0f
refactor(firestore): expose BSONType publicly
ohmayr Sep 23, 2026
8217cd3
feat(firestore): add BSON cross-type query ordering support
ohmayr Sep 16, 2026
fcbc4d1
fix(firestore): streamline BSON cross-type ordering and numeric compa…
ohmayr Sep 21, 2026
9874bff
fix(firestore): resolve lint E741 and format in compare_numbers
ohmayr Sep 21, 2026
705274a
fix(firestore): separate BSON_TIMESTAMP in TypeOrder and optimize com…
ohmayr Sep 23, 2026
d079139
feat(firestore): add PyMongo duck-typing serialization support
ohmayr Sep 16, 2026
0107a0c
feat(firestore): optimize PyMongo duck typing serialization and deleg…
ohmayr Sep 21, 2026
977f9c0
fix(firestore): preserve Binary subtype and support int flags in duck…
ohmayr Sep 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,14 @@ replacements:
from google.cloud.firestore_v1.batch import WriteBatch
from google.cloud.firestore_v1.bson import (
BSONBinary,
BSONDecimal128,
BSONInt32,
BSONMaxKey,
BSONMinKey,
BSONObjectId,
BSONRegex,
BSONTimestamp,
BSONType,
)
from google.cloud.firestore_v1.client import Client
from google.cloud.firestore_v1.collection import CollectionReference
Expand Down Expand Up @@ -179,11 +182,14 @@ replacements:
"AsyncTransaction",
"AsyncWriteBatch",
"BSONBinary",
"BSONDecimal128",
"BSONInt32",
"BSONMaxKey",
"BSONMinKey",
"BSONObjectId",
"BSONRegex",
"BSONTimestamp",
"BSONType",
"Client",
"CountAggregation",
"CollectionGroup",
Expand Down Expand Up @@ -257,11 +263,14 @@ replacements:
AsyncTransaction,
AsyncWriteBatch,
BSONBinary,
BSONDecimal128,
BSONInt32,
BSONMaxKey,
BSONMinKey,
BSONObjectId,
BSONRegex,
BSONTimestamp,
BSONType,
Client,
CollectionGroup,
CollectionReference,
Expand Down Expand Up @@ -320,11 +329,14 @@ replacements:
"AsyncTransaction",
"AsyncWriteBatch",
"BSONBinary",
"BSONDecimal128",
"BSONInt32",
"BSONMaxKey",
"BSONMinKey",
"BSONObjectId",
"BSONRegex",
"BSONTimestamp",
"BSONType",
"Client",
"CountAggregation",
"CollectionGroup",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,14 @@
AsyncTransaction,
AsyncWriteBatch,
BSONBinary,
BSONDecimal128,
BSONInt32,
BSONMaxKey,
BSONMinKey,
BSONObjectId,
BSONRegex,
BSONTimestamp,
BSONType,
Client,
CollectionGroup,
CollectionReference,
Expand Down Expand Up @@ -99,11 +102,14 @@
"AsyncTransaction",
"AsyncWriteBatch",
"BSONBinary",
"BSONDecimal128",
"BSONInt32",
"BSONMaxKey",
"BSONMinKey",
"BSONObjectId",
"BSONRegex",
"BSONTimestamp",
"BSONType",
"Client",
"CountAggregation",
"CollectionGroup",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,14 @@
from google.cloud.firestore_v1.batch import WriteBatch
from google.cloud.firestore_v1.bson import (
BSONBinary,
BSONDecimal128,
BSONInt32,
BSONMaxKey,
BSONMinKey,
BSONObjectId,
BSONRegex,
BSONTimestamp,
BSONType,
)
from google.cloud.firestore_v1.client import Client
from google.cloud.firestore_v1.collection import CollectionReference
Expand Down Expand Up @@ -156,11 +159,14 @@
"AsyncTransaction",
"AsyncWriteBatch",
"BSONBinary",
"BSONDecimal128",
"BSONInt32",
"BSONMaxKey",
"BSONMinKey",
"BSONObjectId",
"BSONRegex",
"BSONTimestamp",
"BSONType",
"Client",
"CountAggregation",
"CollectionGroup",
Expand Down
124 changes: 113 additions & 11 deletions packages/google-cloud-firestore/google/cloud/firestore_v1/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import datetime
import json
import re
from typing import (
TYPE_CHECKING,
Any,
Expand All @@ -43,8 +44,8 @@

import google
from google.cloud import exceptions # type: ignore
from google.cloud.firestore_v1 import transforms, types
from google.cloud.firestore_v1.bson import _BSONType
from google.cloud.firestore_v1 import bson, transforms, types
from google.cloud.firestore_v1.bson import BSONType
from google.cloud.firestore_v1.field_path import FieldPath, parse_field_path
from google.cloud.firestore_v1.types import common, document, write
from google.cloud.firestore_v1.types.write import DocumentTransform
Expand Down Expand Up @@ -170,15 +171,19 @@ def encode_value(value) -> types.document.Value:
Args:
value (Union[NoneType, bool, int, float, datetime.datetime, \
str, bytes, dict, ~google.cloud.Firestore.GeoPoint, \
~google.cloud.firestore_v1.vector.Vector]): A native
Python value to convert to a protobuf field.
~google.cloud.firestore_v1.vector.Vector, \
~google.cloud.firestore_v1.bson._BSONType]): A native \
Python value or supported BSON / PyMongo-compatible value to \
convert to a protobuf field.

Returns:
~google.cloud.firestore_v1.types.Value: A
value encoded as a Firestore protobuf.

Raises:
TypeError: If the ``value`` is not one of the accepted types.
ValueError: If a BSON or duck-typed BSON value has an invalid value
or representation (e.g. invalid ObjectId hex or binary subtype).
"""
if value is None:
return document.Value(null_value=struct_pb2.NULL_VALUE)
Expand All @@ -203,6 +208,9 @@ def encode_value(value) -> types.document.Value:
return document.Value(string_value=value)

if isinstance(value, bytes):
subtype = getattr(value, "subtype", None)
if subtype is not None:
return encode_value(bson.BSONBinary(value, subtype=subtype)._to_map_value())
return document.Value(bytes_value=value)

# NOTE: We avoid doing an isinstance() check for a Document
Expand All @@ -211,7 +219,7 @@ def encode_value(value) -> types.document.Value:
if document_path is not None:
return document.Value(reference_value=document_path)

if isinstance(value, _BSONType):
if isinstance(value, BSONType):
return encode_value(value._to_map_value())

if isinstance(value, GeoPoint):
Expand All @@ -230,11 +238,73 @@ def encode_value(value) -> types.document.Value:
value_pb = document.MapValue(fields=value_dict)
return document.Value(map_value=value_pb)

# Fallback: Coerce third-party BSON objects (e.g. PyMongo) to Firestore BSON types
bson_val = _try_duck_type_bson(value)
if bson_val is not None:
return encode_value(bson_val._to_map_value())

raise TypeError(
"Cannot convert to a Firestore Value", value, "Invalid type", type(value)
)


# Mapping of Python standard library regex flags to their canonical BSON regex
# option characters per the BSON specification (https://bsonspec.org/spec.html, type 0x0B).
# Stored in alphabetical order of option characters to produce normalized output.
_REGEX_FLAG_TO_BSON_CHAR: Tuple[Tuple[int, str], ...] = (
(re.IGNORECASE, "i"), # Case-insensitive matching
(re.LOCALE, "l"), # Locale-dependent matching
(re.MULTILINE, "m"), # Multi-line matching
(re.DOTALL, "s"), # Dot matches all (including newline)
(re.UNICODE, "u"), # Unicode matching
(re.VERBOSE, "x"), # Verbose / whitespace-ignored matching
)


def _flags_to_options_string(flags: Any) -> str:
"""Convert regex flags to a normalized BSON options string.

Supports string options directly (e.g. ``"i"``), integer bitmasks from
the standard library ``re`` module (e.g. ``re.IGNORECASE | re.MULTILINE``),
or third-party driver types like PyMongo's ``Regex.flags``.

Args:
flags (Any): A string of flag characters or an integer bitmask of regex flags.

Returns:
str: The corresponding BSON regex options string.
"""
if isinstance(flags, str):
return flags
if isinstance(flags, int):
return "".join(char for flag, char in _REGEX_FLAG_TO_BSON_CHAR if flags & flag)
return str(flags)


def _try_duck_type_bson(value) -> Optional[BSONType]:
"""Coerce third-party BSON objects (e.g. PyMongo) to Firestore BSON types."""
cls_name = getattr(value.__class__, "__name__", "")
if cls_name == "ObjectId" and hasattr(value, "binary"):
return bson.BSONObjectId(str(value).lower())
if cls_name == "Decimal128" and hasattr(value, "to_decimal"):
return bson.BSONDecimal128(value.to_decimal())
if cls_name == "Regex" and hasattr(value, "pattern"):
raw_opts = getattr(value, "flags", None)
if raw_opts is None or raw_opts == "":
raw_opts = getattr(value, "options", "")
opts = _flags_to_options_string(raw_opts)
return bson.BSONRegex(value.pattern, opts)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can opts here be non-string? Gemini flagged this as a potential issue

(I know I started a thread suggesting to not support int, but I don't feel too strongly one way or the other if it would make things easier here. We just need to make sure we're communicating what flags we accept and what we reject clearly)

if cls_name == "Timestamp" and hasattr(value, "time") and hasattr(value, "inc"):
return bson.BSONTimestamp(value.time, value.inc)
if cls_name == "MinKey":
return bson.BSONMinKey()
if cls_name == "MaxKey":
return bson.BSONMaxKey()
if cls_name == "Binary" and hasattr(value, "subtype"):
return bson.BSONBinary(value, subtype=value.subtype)
return None


def encode_dict(values_dict) -> dict:
"""Encode a dictionary into protobuf ``Value``-s.

Expand Down Expand Up @@ -350,7 +420,18 @@ def reference_value_to_document(reference_value, client) -> Any:
def decode_value(
value, client
) -> Union[
None, bool, int, float, list, datetime.datetime, str, bytes, dict, GeoPoint, Vector
None,
bool,
int,
float,
list,
datetime.datetime,
str,
bytes,
dict,
GeoPoint,
Vector,
BSONType,
]:
"""Converts a Firestore protobuf ``Value`` to a native Python value.

Expand All @@ -362,7 +443,9 @@ def decode_value(

Returns:
Union[NoneType, bool, int, float, datetime.datetime, \
str, bytes, dict, ~google.cloud.Firestore.GeoPoint]: A native
str, bytes, dict, ~google.cloud.Firestore.GeoPoint, \
~google.cloud.firestore_v1.vector.Vector, \
~google.cloud.firestore_v1.bson.BSONType]: A native \
Python value converted from the ``value``.

Raises:
Expand Down Expand Up @@ -402,7 +485,22 @@ def decode_value(
raise ValueError("Unknown ``value_type``", value_type)


def decode_dict(value_fields, client) -> Union[dict, Vector]:
def _decode_bson_dict_recursive(data: Any) -> Any:
"""Recursively decodes BSON wire map dictionaries."""
if isinstance(data, dict):
decoded = BSONType._from_dict(data)
if decoded is not None:
return decoded
return {k: _decode_bson_dict_recursive(v) for k, v in data.items()}
elif isinstance(data, list):
return [_decode_bson_dict_recursive(item) for item in data]
return data


def decode_dict(
value_fields,
client,
) -> Union[dict, Vector, BSONType]:
"""Converts a protobuf map of Firestore ``Value``-s.

Args:
Expand All @@ -412,9 +510,9 @@ def decode_dict(value_fields, client) -> Union[dict, Vector]:
A client that has a document factory.

Returns:
Dict[str, Union[NoneType, bool, int, float, datetime.datetime, \
str, bytes, dict, ~google.cloud.Firestore.GeoPoint]]: A dictionary
of native Python values converted from the ``value_fields``.
Union[dict, ~google.cloud.firestore_v1.vector.Vector, \
~google.cloud.firestore_v1.bson.BSONType]: A dictionary of native \
Python values, Vector, or BSON object converted from ``value_fields``.
"""
value_fields_pb = getattr(value_fields, "_pb", value_fields)
res = {key: decode_value(value, client) for key, value in value_fields_pb.items()}
Expand All @@ -425,6 +523,10 @@ def decode_dict(value_fields, client) -> Union[dict, Vector]:
values = cast(Sequence[float], res["value"])
return Vector(values)

decoded = BSONType._from_dict(res)
if decoded is not None:
return decoded

return res


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,8 @@ def to_dict(self) -> Union[Dict[str, Any], None]:
"""
if not self._exists:
return None
return copy.deepcopy(self._data)
data = copy.deepcopy(self._data)
return _helpers._decode_bson_dict_recursive(data)

def _to_protobuf(self) -> Optional[Document]:
return _helpers.document_snapshot_to_protobuf(self)
Expand Down
Loading
Loading