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