1# Copyright 2017 Google LLC All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import math
16from enum import Enum
17from typing import Any
18
19from google.cloud.firestore_v1._helpers import decode_value
20from google.cloud.firestore_v1._helpers import GeoPoint
21
22
23class TypeOrder(Enum):
24 """The supported Data Type.
25
26 Note: The Enum value does not imply the sort order.
27 """
28
29 NULL = 0
30 BOOLEAN = 1
31 NUMBER = 2
32 TIMESTAMP = 3
33 STRING = 4
34 BLOB = 5
35 REF = 6
36 GEO_POINT = 7
37 ARRAY = 8
38 OBJECT = 9
39 VECTOR = 10
40
41 @staticmethod
42 def from_value(value) -> Any:
43 v = value._pb.WhichOneof("value_type")
44 lut = {
45 "null_value": TypeOrder.NULL,
46 "boolean_value": TypeOrder.BOOLEAN,
47 "integer_value": TypeOrder.NUMBER,
48 "double_value": TypeOrder.NUMBER,
49 "timestamp_value": TypeOrder.TIMESTAMP,
50 "string_value": TypeOrder.STRING,
51 "bytes_value": TypeOrder.BLOB,
52 "reference_value": TypeOrder.REF,
53 "geo_point_value": TypeOrder.GEO_POINT,
54 "array_value": TypeOrder.ARRAY,
55 "map_value": TypeOrder.OBJECT,
56 }
57
58 if v not in lut:
59 raise ValueError(f"Could not detect value type for {v}")
60
61 if v == "map_value":
62 if (
63 "__type__" in value.map_value.fields
64 and value.map_value.fields["__type__"].string_value == "__vector__"
65 ):
66 return TypeOrder.VECTOR
67 return lut[v]
68
69
70# NOTE: This order is defined by the backend and cannot be changed.
71_TYPE_ORDER_MAP = {
72 TypeOrder.NULL: 0,
73 TypeOrder.BOOLEAN: 1,
74 TypeOrder.NUMBER: 2,
75 TypeOrder.TIMESTAMP: 3,
76 TypeOrder.STRING: 4,
77 TypeOrder.BLOB: 5,
78 TypeOrder.REF: 6,
79 TypeOrder.GEO_POINT: 7,
80 TypeOrder.ARRAY: 8,
81 TypeOrder.VECTOR: 9,
82 TypeOrder.OBJECT: 10,
83}
84
85
86class Order(object):
87 """
88 Order implements the ordering semantics of the backend.
89 """
90
91 @classmethod
92 def compare(cls, left, right) -> int:
93 """
94 Main comparison function for all Firestore types.
95 @return -1 is left < right, 0 if left == right, otherwise 1
96 """
97 # First compare the types.
98 leftType = TypeOrder.from_value(left)
99 rightType = TypeOrder.from_value(right)
100 if leftType != rightType:
101 if _TYPE_ORDER_MAP[leftType] < _TYPE_ORDER_MAP[rightType]:
102 return -1
103 else:
104 return 1
105
106 if leftType == TypeOrder.NULL:
107 return 0 # nulls are all equal
108 elif leftType == TypeOrder.BOOLEAN:
109 return cls._compare_to(left.boolean_value, right.boolean_value)
110 elif leftType == TypeOrder.NUMBER:
111 return cls.compare_numbers(left, right)
112 elif leftType == TypeOrder.TIMESTAMP:
113 return cls.compare_timestamps(left, right)
114 elif leftType == TypeOrder.STRING:
115 return cls._compare_to(left.string_value, right.string_value)
116 elif leftType == TypeOrder.BLOB:
117 return cls.compare_blobs(left, right)
118 elif leftType == TypeOrder.REF:
119 return cls.compare_resource_paths(left, right)
120 elif leftType == TypeOrder.GEO_POINT:
121 return cls.compare_geo_points(left, right)
122 elif leftType == TypeOrder.ARRAY:
123 return cls.compare_arrays(left, right)
124 elif leftType == TypeOrder.VECTOR:
125 # ARRAYs < VECTORs < MAPs
126 return cls.compare_vectors(left, right)
127 elif leftType == TypeOrder.OBJECT:
128 return cls.compare_objects(left, right)
129 else:
130 raise ValueError(f"Unknown TypeOrder {leftType}")
131
132 @staticmethod
133 def compare_blobs(left, right) -> int:
134 left_bytes = left.bytes_value
135 right_bytes = right.bytes_value
136
137 return Order._compare_to(left_bytes, right_bytes)
138
139 @staticmethod
140 def compare_timestamps(left, right) -> Any:
141 left = left._pb.timestamp_value
142 right = right._pb.timestamp_value
143
144 seconds = Order._compare_to(left.seconds or 0, right.seconds or 0)
145 if seconds != 0:
146 return seconds
147
148 return Order._compare_to(left.nanos or 0, right.nanos or 0)
149
150 @staticmethod
151 def compare_geo_points(left, right) -> Any:
152 left_value = decode_value(left, None)
153 right_value = decode_value(right, None)
154 if not isinstance(left_value, GeoPoint) or not isinstance(
155 right_value, GeoPoint
156 ):
157 raise AttributeError("invalid geopoint encountered")
158 cmp = (left_value.latitude > right_value.latitude) - (
159 left_value.latitude < right_value.latitude
160 )
161
162 if cmp != 0:
163 return cmp
164 return (left_value.longitude > right_value.longitude) - (
165 left_value.longitude < right_value.longitude
166 )
167
168 @staticmethod
169 def compare_resource_paths(left, right) -> int:
170 left = left.reference_value
171 right = right.reference_value
172
173 left_segments = left.split("/")
174 right_segments = right.split("/")
175 shorter = min(len(left_segments), len(right_segments))
176 # compare segments
177 for i in range(shorter):
178 if left_segments[i] < right_segments[i]:
179 return -1
180 if left_segments[i] > right_segments[i]:
181 return 1
182
183 left_length = len(left)
184 right_length = len(right)
185 return (left_length > right_length) - (left_length < right_length)
186
187 @staticmethod
188 def compare_arrays(left, right) -> int:
189 l_values = left.array_value.values
190 r_values = right.array_value.values
191
192 length = min(len(l_values), len(r_values))
193 for i in range(length):
194 cmp = Order.compare(l_values[i], r_values[i])
195 if cmp != 0:
196 return cmp
197
198 return Order._compare_to(len(l_values), len(r_values))
199
200 @staticmethod
201 def compare_vectors(left, right) -> int:
202 # First compare the size of vector.
203 l_values = left.map_value.fields["value"]
204 r_values = right.map_value.fields["value"]
205
206 left_length = len(l_values.array_value.values)
207 right_length = len(r_values.array_value.values)
208
209 if left_length != right_length:
210 return Order._compare_to(left_length, right_length)
211
212 # Compare element if the size matches.
213 return Order.compare_arrays(l_values, r_values)
214
215 @staticmethod
216 def compare_objects(left, right) -> int:
217 left_fields = left.map_value.fields
218 right_fields = right.map_value.fields
219
220 for left_key, right_key in zip(sorted(left_fields), sorted(right_fields)):
221 keyCompare = Order._compare_to(left_key, right_key)
222 if keyCompare != 0:
223 return keyCompare
224
225 value_compare = Order.compare(
226 left_fields[left_key], right_fields[right_key]
227 )
228 if value_compare != 0:
229 return value_compare
230
231 return Order._compare_to(len(left_fields), len(right_fields))
232
233 @staticmethod
234 def compare_numbers(left, right) -> int:
235 left_value = decode_value(left, None)
236 right_value = decode_value(right, None)
237 return Order.compare_doubles(left_value, right_value)
238
239 @staticmethod
240 def compare_doubles(left, right) -> int:
241 if math.isnan(left):
242 if math.isnan(right):
243 return 0
244 return -1
245 if math.isnan(right):
246 return 1
247
248 return Order._compare_to(left, right)
249
250 @staticmethod
251 def _compare_to(left, right) -> int:
252 # We can't just use cmp(left, right) because cmp doesn't exist
253 # in Python 3, so this is an equivalent suggested by
254 # https://docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons
255 return (left > right) - (left < right)