2020from google .cloud .firestore_v1 ._helpers import GeoPoint , decode_value
2121
2222
23+ def _to_number (val : Any ) -> Any :
24+ """Extract a numeric value (int, float, Decimal) from a Value protobuf or Python value.
25+
26+ Directly inspects the protobuf value_type without calling decode_value()
27+ for optimal performance.
28+ """
29+ value_pb = getattr (val , "_pb" , val )
30+ which = (
31+ value_pb .WhichOneof ("value_type" ) if hasattr (value_pb , "WhichOneof" ) else None
32+ )
33+
34+ if which == "integer_value" :
35+ return value_pb .integer_value
36+ elif which == "double_value" :
37+ return value_pb .double_value
38+ elif which == "map_value" :
39+ fields = value_pb .map_value .fields
40+ if "__int__" in fields :
41+ return fields ["__int__" ].integer_value
42+ elif "__decimal128__" in fields :
43+ return decimal .Decimal (fields ["__decimal128__" ].string_value )
44+
45+ num = decode_value (val , None )
46+ to_decimal = getattr (num , "to_decimal" , None )
47+ return to_decimal () if callable (to_decimal ) else getattr (num , "value" , num )
48+
49+
50+ def _is_nan (val : Any ) -> bool :
51+ """Check if a numeric value is NaN, safely handling OverflowError and non-floats."""
52+ if hasattr (val , "is_nan" ):
53+ return val .is_nan ()
54+ if isinstance (val , (int , decimal .Decimal )):
55+ return False
56+ try :
57+ return math .isnan (val )
58+ except (TypeError , OverflowError ):
59+ return False
60+
61+
2362class TypeOrder (Enum ):
2463 """The supported Data Type.
2564
@@ -42,6 +81,7 @@ class TypeOrder(Enum):
4281 BSON_OBJECT_ID = 13
4382 BSON_BINARY = 14
4483 BSON_REGEX = 15
84+ BSON_TIMESTAMP = 16
4585
4686 @staticmethod
4787 def from_value (value ) -> Any :
@@ -77,41 +117,38 @@ def from_value(value) -> Any:
77117
78118
79119# Maps BSON wire map keys directly to their corresponding TypeOrder.
80- # BSONTimestamp maps to TypeOrder.TIMESTAMP, and BSONInt32 / BSONDecimal128
81- # map to TypeOrder.NUMBER, enabling cross-type comparisons.
120+ # BSONInt32 and BSONDecimal128 map to TypeOrder.NUMBER, enabling cross-type comparisons.
82121_BSON_KEY_TO_TYPE_ORDER = {
83122 "__min__" : TypeOrder .BSON_MIN_KEY ,
84123 "__max__" : TypeOrder .BSON_MAX_KEY ,
85124 "__oid__" : TypeOrder .BSON_OBJECT_ID ,
86125 "__int__" : TypeOrder .NUMBER ,
87126 "__decimal128__" : TypeOrder .NUMBER ,
88127 "__binary__" : TypeOrder .BSON_BINARY ,
89- "__request_timestamp__" : TypeOrder .TIMESTAMP ,
128+ "__request_timestamp__" : TypeOrder .BSON_TIMESTAMP ,
90129 "__regex__" : TypeOrder .BSON_REGEX ,
91130}
92131
93132
94133# NOTE: This order is defined by the backend and cannot be changed.
95- # BSONTimestamp shares TypeOrder.TIMESTAMP with native timestamps, and
96- # BSONInt32 / BSONDecimal128 share TypeOrder.NUMBER, enabling direct cross-type
97- # value comparison within those categories.
98134_TYPE_ORDER_MAP = {
99135 TypeOrder .NULL : 0 ,
100136 TypeOrder .BSON_MIN_KEY : 1 ,
101137 TypeOrder .BOOLEAN : 2 ,
102138 TypeOrder .NUMBER : 3 ,
103139 TypeOrder .TIMESTAMP : 4 ,
104- TypeOrder .STRING : 5 ,
105- TypeOrder .BLOB : 6 ,
106- TypeOrder .BSON_BINARY : 7 ,
107- TypeOrder .REF : 8 ,
108- TypeOrder .BSON_OBJECT_ID : 9 ,
109- TypeOrder .GEO_POINT : 10 ,
110- TypeOrder .BSON_REGEX : 11 ,
111- TypeOrder .ARRAY : 12 ,
112- TypeOrder .VECTOR : 13 ,
113- TypeOrder .OBJECT : 14 ,
114- TypeOrder .BSON_MAX_KEY : 15 ,
140+ TypeOrder .BSON_TIMESTAMP : 5 ,
141+ TypeOrder .STRING : 6 ,
142+ TypeOrder .BLOB : 7 ,
143+ TypeOrder .BSON_BINARY : 8 ,
144+ TypeOrder .REF : 9 ,
145+ TypeOrder .BSON_OBJECT_ID : 10 ,
146+ TypeOrder .GEO_POINT : 11 ,
147+ TypeOrder .BSON_REGEX : 12 ,
148+ TypeOrder .ARRAY : 13 ,
149+ TypeOrder .VECTOR : 14 ,
150+ TypeOrder .OBJECT : 15 ,
151+ TypeOrder .BSON_MAX_KEY : 16 ,
115152}
116153
117154
@@ -147,8 +184,9 @@ def compare(cls, left, right) -> int:
147184 # Handles int64, double, BSONInt32, and BSONDecimal128.
148185 return cls .compare_numbers (left , right )
149186 elif leftType == TypeOrder .TIMESTAMP :
150- # Handles native Firestore timestamps and BSONTimestamp.
151187 return cls .compare_timestamps (left , right )
188+ elif leftType == TypeOrder .BSON_TIMESTAMP :
189+ return cls .compare_bson_timestamps (left , right )
152190 elif leftType == TypeOrder .STRING :
153191 return cls ._compare_to (left .string_value , right .string_value )
154192 elif leftType == TypeOrder .BLOB :
@@ -219,25 +257,31 @@ def compare_bson_regexes(left, right) -> int:
219257
220258 @staticmethod
221259 def compare_timestamps (left , right ) -> Any :
222- """Compare native Firestore timestamps and BSON timestamps."""
223260 left_pb = getattr (left , "_pb" , left )
224261 right_pb = getattr (right , "_pb" , right )
225262
226- if left_pb .WhichOneof ("value_type" ) == "map_value" :
227- l_ts = left_pb .map_value .fields ["__request_timestamp__" ].map_value .fields
228- l_sec = l_ts ["seconds" ].integer_value if "seconds" in l_ts else 0
229- l_inc = l_ts ["increment" ].integer_value if "increment" in l_ts else 0
230- else :
231- l_sec = left_pb .timestamp_value .seconds
232- l_inc = left_pb .timestamp_value .nanos
263+ seconds = Order ._compare_to (
264+ left_pb .timestamp_value .seconds , right_pb .timestamp_value .seconds
265+ )
266+ if seconds != 0 :
267+ return seconds
233268
234- if right_pb .WhichOneof ("value_type" ) == "map_value" :
235- r_ts = right_pb .map_value .fields ["__request_timestamp__" ].map_value .fields
236- r_sec = r_ts ["seconds" ].integer_value if "seconds" in r_ts else 0
237- r_inc = r_ts ["increment" ].integer_value if "increment" in r_ts else 0
238- else :
239- r_sec = right_pb .timestamp_value .seconds
240- r_inc = right_pb .timestamp_value .nanos
269+ return Order ._compare_to (
270+ left_pb .timestamp_value .nanos , right_pb .timestamp_value .nanos
271+ )
272+
273+ @staticmethod
274+ def compare_bson_timestamps (left , right ) -> Any :
275+ left_pb = getattr (left , "_pb" , left )
276+ right_pb = getattr (right , "_pb" , right )
277+
278+ l_ts = left_pb .map_value .fields ["__request_timestamp__" ].map_value .fields
279+ l_sec = l_ts ["seconds" ].integer_value if "seconds" in l_ts else 0
280+ l_inc = l_ts ["increment" ].integer_value if "increment" in l_ts else 0
281+
282+ r_ts = right_pb .map_value .fields ["__request_timestamp__" ].map_value .fields
283+ r_sec = r_ts ["seconds" ].integer_value if "seconds" in r_ts else 0
284+ r_inc = r_ts ["increment" ].integer_value if "increment" in r_ts else 0
241285
242286 seconds = Order ._compare_to (l_sec , r_sec )
243287 if seconds != 0 :
@@ -331,23 +375,11 @@ def compare_objects(left, right) -> int:
331375 @staticmethod
332376 def compare_numbers (left , right ) -> int :
333377 """Compare numeric values across int, float, BSONInt32, and BSONDecimal128."""
334-
335- def _to_number (val ):
336- num = decode_value (val , None )
337- to_decimal = getattr (num , "to_decimal" , None )
338- return to_decimal () if callable (to_decimal ) else getattr (num , "value" , num )
339-
340378 left_val = _to_number (left )
341379 right_val = _to_number (right )
342380
343- left_nan = (
344- left_val .is_nan () if hasattr (left_val , "is_nan" ) else math .isnan (left_val )
345- )
346- right_nan = (
347- right_val .is_nan ()
348- if hasattr (right_val , "is_nan" )
349- else math .isnan (right_val )
350- )
381+ left_nan = _is_nan (left_val )
382+ right_nan = _is_nan (right_val )
351383 if left_nan or right_nan :
352384 return 0 if (left_nan and right_nan ) else (- 1 if left_nan else 1 )
353385
0 commit comments