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)