1# Copyright 2024 Google LLC 
    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. 
    14from __future__ import annotations 
    15 
    16from typing import Any 
    17 
    18import datetime 
    19 
    20from dataclasses import dataclass 
    21from google.protobuf.json_format import MessageToDict 
    22 
    23 
    24@dataclass(frozen=True) 
    25class ExplainOptions: 
    26    """ 
    27    Explain options for the query. 
    28    Set on a query object using the explain_options attribute at query 
    29    construction time. 
    30 
    31    :type analyze: bool 
    32    :param analyze: Optional. Whether to execute this query. When false 
    33    (the default), the query will be planned, returning only metrics from the 
    34    planning stages. When true, the query will be planned and executed, 
    35    returning the full query results along with both planning and execution 
    36    stage metrics. 
    37    """ 
    38 
    39    analyze: bool = False 
    40 
    41    def _to_dict(self): 
    42        return {"analyze": self.analyze} 
    43 
    44 
    45@dataclass(frozen=True) 
    46class PlanSummary: 
    47    """ 
    48    Contains planning phase information about a query.` 
    49 
    50    :type indexes_used: list[dict[str, Any]] 
    51    :param indexes_used: The indexes selected for this query. 
    52    """ 
    53 
    54    indexes_used: list[dict[str, Any]] 
    55 
    56 
    57@dataclass(frozen=True) 
    58class ExecutionStats: 
    59    """ 
    60    Execution phase information about a query. 
    61 
    62    Only available when explain_options.analyze is True. 
    63 
    64    :type results_returned: int 
    65    :param results_returned: Total number of results returned, including 
    66        documents, projections, aggregation results, keys. 
    67    :type execution_duration: datetime.timedelta 
    68    :param execution_duration: Total time to execute the query in the backend. 
    69    :type read_operations: int 
    70    :param read_operations: Total billable read operations. 
    71    :type debug_stats: dict[str, Any] 
    72    :param debug_stats: Debugging statistics from the execution of the query. 
    73        Note that the debugging stats are subject to change as Firestore evolves 
    74    """ 
    75 
    76    results_returned: int 
    77    execution_duration: datetime.timedelta 
    78    read_operations: int 
    79    debug_stats: dict[str, Any] 
    80 
    81 
    82@dataclass(frozen=True) 
    83class ExplainMetrics: 
    84    """ 
    85    ExplainMetrics contains information about the planning and execution of a query. 
    86 
    87    When explain_options.analyze is false, only plan_summary is available. 
    88    When explain_options.analyze is true, execution_stats is also available. 
    89 
    90    :type plan_summary: PlanSummary 
    91    :param plan_summary: Planning phase information about the query. 
    92    :type execution_stats: ExecutionStats 
    93    :param execution_stats: Execution phase information about the query. 
    94    """ 
    95 
    96    plan_summary: PlanSummary 
    97 
    98    @staticmethod 
    99    def _from_pb(metrics_pb): 
    100        dict_repr = MessageToDict(metrics_pb._pb, preserving_proto_field_name=True) 
    101        plan_summary = PlanSummary( 
    102            indexes_used=dict_repr.get("plan_summary", {}).get("indexes_used", []) 
    103        ) 
    104        if "execution_stats" in dict_repr: 
    105            stats_dict = dict_repr.get("execution_stats", {}) 
    106            execution_stats = ExecutionStats( 
    107                results_returned=int(stats_dict.get("results_returned", 0)), 
    108                execution_duration=metrics_pb.execution_stats.execution_duration, 
    109                read_operations=int(stats_dict.get("read_operations", 0)), 
    110                debug_stats=stats_dict.get("debug_stats", {}), 
    111            ) 
    112            return _ExplainAnalyzeMetrics( 
    113                plan_summary=plan_summary, _execution_stats=execution_stats 
    114            ) 
    115        else: 
    116            return ExplainMetrics(plan_summary=plan_summary) 
    117 
    118    @property 
    119    def execution_stats(self) -> ExecutionStats: 
    120        raise QueryExplainError( 
    121            "execution_stats not available when explain_options.analyze=False." 
    122        ) 
    123 
    124 
    125@dataclass(frozen=True) 
    126class _ExplainAnalyzeMetrics(ExplainMetrics): 
    127    """ 
    128    Subclass of ExplainMetrics that includes execution_stats. 
    129    Only available when explain_options.analyze is True. 
    130    """ 
    131 
    132    plan_summary: PlanSummary 
    133    _execution_stats: ExecutionStats 
    134 
    135    @property 
    136    def execution_stats(self) -> ExecutionStats: 
    137        return self._execution_stats 
    138 
    139 
    140class QueryExplainError(Exception): 
    141    """ 
    142    Error returned when there is a problem accessing query profiling information. 
    143    """ 
    144 
    145    pass