13
13
# limitations under the License.
14
14
15
15
"""Create / interact with Google Cloud Datastore queries."""
16
-
17
16
import base64
18
17
import warnings
19
18
20
-
21
19
from google .api_core import page_iterator
22
20
from google .cloud ._helpers import _ensure_tuple_or_list
23
21
24
-
25
22
from google .cloud .datastore_v1 .types import entity as entity_pb2
26
23
from google .cloud .datastore_v1 .types import query as query_pb2
27
24
from google .cloud .datastore import helpers
28
25
from google .cloud .datastore .key import Key
29
26
27
+
28
+ from google .cloud .datastore .query_profile import ExplainMetrics
29
+ from google .cloud .datastore .query_profile import QueryExplainError
30
+
30
31
import abc
31
32
from abc import ABC
32
33
38
39
_NO_MORE_RESULTS ,
39
40
query_pb2 .QueryResultBatch .MoreResultsType .MORE_RESULTS_AFTER_LIMIT ,
40
41
query_pb2 .QueryResultBatch .MoreResultsType .MORE_RESULTS_AFTER_CURSOR ,
42
+ query_pb2 .QueryResultBatch .MoreResultsType .MORE_RESULTS_TYPE_UNSPECIFIED , # received when explain_options(analyze=False)
41
43
)
42
44
43
45
KEY_PROPERTY_NAME = "__key__"
@@ -176,6 +178,11 @@ class Query(object):
176
178
:type distinct_on: sequence of string
177
179
:param distinct_on: field names used to group query results.
178
180
181
+ :type explain_options: :class:`~google.cloud.datastore.ExplainOptions`
182
+ :param explain_options: (Optional) Options to enable query profiling for
183
+ this query. When set, explain_metrics will be available on the iterator
184
+ returned by query.fetch().
185
+
179
186
:raises: ValueError if ``project`` is not passed and no implicit
180
187
default is set.
181
188
"""
@@ -203,6 +210,7 @@ def __init__(
203
210
projection = (),
204
211
order = (),
205
212
distinct_on = (),
213
+ explain_options = None ,
206
214
):
207
215
self ._client = client
208
216
self ._kind = kind
@@ -221,6 +229,7 @@ def __init__(
221
229
else :
222
230
self ._namespace = None
223
231
232
+ self ._explain_options = explain_options
224
233
self ._ancestor = ancestor
225
234
self ._filters = []
226
235
@@ -704,6 +713,7 @@ def __init__(
704
713
self ._timeout = timeout
705
714
self ._read_time = read_time
706
715
# The attributes below will change over the life of the iterator.
716
+ self ._explain_metrics = None
707
717
self ._more_results = True
708
718
self ._skipped_results = 0
709
719
@@ -777,7 +787,6 @@ def _next_page(self):
777
787
if not self ._more_results :
778
788
return None
779
789
780
- query_pb = self ._build_protobuf ()
781
790
new_transaction_options = None
782
791
transaction_id , new_transaction_options = helpers .get_transaction_options (
783
792
self .client .current_transaction
@@ -804,46 +813,70 @@ def _next_page(self):
804
813
"project_id" : self ._query .project ,
805
814
"partition_id" : partition_id ,
806
815
"read_options" : read_options ,
807
- "query" : query_pb ,
816
+ "query" : self . _build_protobuf () ,
808
817
}
818
+ if self ._query ._explain_options :
819
+ request ["explain_options" ] = self ._query ._explain_options ._to_dict ()
809
820
810
821
helpers .set_database_id_to_request (request , self .client .database )
811
822
812
- response_pb = self .client ._datastore_api .run_query (
813
- request = request ,
814
- ** kwargs ,
815
- )
823
+ response_pb = None
816
824
817
- while (
825
+ while response_pb is None or (
818
826
response_pb .batch .more_results == _NOT_FINISHED
819
- and response_pb .batch .skipped_results < query_pb .offset
827
+ and response_pb .batch .skipped_results < request [ "query" ] .offset
820
828
):
821
- # We haven't finished processing. A likely reason is we haven't
822
- # skipped all of the results yet. Don't return any results.
823
- # Instead, rerun query, adjusting offsets. Datastore doesn't process
824
- # more than 1000 skipped results in a query.
825
- old_query_pb = query_pb
826
- query_pb = query_pb2 .Query ()
827
- query_pb ._pb .CopyFrom (old_query_pb ._pb ) # copy for testability
828
- query_pb .start_cursor = response_pb .batch .end_cursor
829
- query_pb .offset -= response_pb .batch .skipped_results
830
-
831
- request = {
832
- "project_id" : self ._query .project ,
833
- "partition_id" : partition_id ,
834
- "read_options" : read_options ,
835
- "query" : query_pb ,
836
- }
837
- helpers .set_database_id_to_request (request , self .client .database )
829
+ if response_pb is not None :
830
+ # We haven't finished processing. A likely reason is we haven't
831
+ # skipped all of the results yet. Don't return any results.
832
+ # Instead, rerun query, adjusting offsets. Datastore doesn't process
833
+ # more than 1000 skipped results in a query.
834
+ new_query_pb = query_pb2 .Query ()
835
+ new_query_pb ._pb .CopyFrom (request ["query" ]._pb ) # copy for testability
836
+ new_query_pb .start_cursor = response_pb .batch .end_cursor
837
+ new_query_pb .offset -= response_pb .batch .skipped_results
838
+ request ["query" ] = new_query_pb
838
839
839
840
response_pb = self .client ._datastore_api .run_query (
840
- request = request ,
841
- ** kwargs ,
841
+ request = request .copy (), ** kwargs
842
842
)
843
+ # capture explain metrics if present in response
844
+ # should only be present in last response, and only if explain_options was set
845
+ if response_pb and response_pb .explain_metrics :
846
+ self ._explain_metrics = ExplainMetrics ._from_pb (
847
+ response_pb .explain_metrics
848
+ )
843
849
844
850
entity_pbs = self ._process_query_results (response_pb )
845
851
return page_iterator .Page (self , entity_pbs , self .item_to_value )
846
852
853
+ @property
854
+ def explain_metrics (self ) -> ExplainMetrics :
855
+ """
856
+ Get the metrics associated with the query execution.
857
+ Metrics are only available when explain_options is set on the query. If
858
+ ExplainOptions.analyze is False, only plan_summary is available. If it is
859
+ True, execution_stats is also available.
860
+
861
+ :rtype: :class:`~google.cloud.datastore.query_profile.ExplainMetrics`
862
+ :returns: The metrics associated with the query execution.
863
+ :raises: :class:`~google.cloud.datastore.query_profile.QueryExplainError`
864
+ if explain_metrics is not available on the query.
865
+ """
866
+ if self ._explain_metrics is not None :
867
+ return self ._explain_metrics
868
+ elif self ._query ._explain_options is None :
869
+ raise QueryExplainError ("explain_options not set on query." )
870
+ elif self ._query ._explain_options .analyze is False :
871
+ # we need to run the query to get the explain_metrics
872
+ # analyze=False only returns explain_metrics, no results
873
+ self ._next_page ()
874
+ if self ._explain_metrics is not None :
875
+ return self ._explain_metrics
876
+ raise QueryExplainError (
877
+ "explain_metrics not available until query is complete."
878
+ )
879
+
847
880
848
881
def _pb_from_query (query ):
849
882
"""Convert a Query instance to the corresponding protobuf.
0 commit comments