1313# limitations under the License.
1414
1515"""Create / interact with Google Cloud Datastore queries."""
16-
1716import base64
1817import warnings
1918
20-
2119from google .api_core import page_iterator
2220from google .cloud ._helpers import _ensure_tuple_or_list
2321
24-
2522from google .cloud .datastore_v1 .types import entity as entity_pb2
2623from google .cloud .datastore_v1 .types import query as query_pb2
2724from google .cloud .datastore import helpers
2825from google .cloud .datastore .key import Key
2926
27+
28+ from google .cloud .datastore .query_profile import ExplainMetrics
29+ from google .cloud .datastore .query_profile import QueryExplainError
30+
3031import abc
3132from abc import ABC
3233
3839 _NO_MORE_RESULTS ,
3940 query_pb2 .QueryResultBatch .MoreResultsType .MORE_RESULTS_AFTER_LIMIT ,
4041 query_pb2 .QueryResultBatch .MoreResultsType .MORE_RESULTS_AFTER_CURSOR ,
42+ query_pb2 .QueryResultBatch .MoreResultsType .MORE_RESULTS_TYPE_UNSPECIFIED , # received when explain_options(analyze=False)
4143)
4244
4345KEY_PROPERTY_NAME = "__key__"
@@ -176,6 +178,11 @@ class Query(object):
176178 :type distinct_on: sequence of string
177179 :param distinct_on: field names used to group query results.
178180
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+
179186 :raises: ValueError if ``project`` is not passed and no implicit
180187 default is set.
181188 """
@@ -203,6 +210,7 @@ def __init__(
203210 projection = (),
204211 order = (),
205212 distinct_on = (),
213+ explain_options = None ,
206214 ):
207215 self ._client = client
208216 self ._kind = kind
@@ -221,6 +229,7 @@ def __init__(
221229 else :
222230 self ._namespace = None
223231
232+ self ._explain_options = explain_options
224233 self ._ancestor = ancestor
225234 self ._filters = []
226235
@@ -704,6 +713,7 @@ def __init__(
704713 self ._timeout = timeout
705714 self ._read_time = read_time
706715 # The attributes below will change over the life of the iterator.
716+ self ._explain_metrics = None
707717 self ._more_results = True
708718 self ._skipped_results = 0
709719
@@ -777,7 +787,6 @@ def _next_page(self):
777787 if not self ._more_results :
778788 return None
779789
780- query_pb = self ._build_protobuf ()
781790 new_transaction_options = None
782791 transaction_id , new_transaction_options = helpers .get_transaction_options (
783792 self .client .current_transaction
@@ -804,46 +813,70 @@ def _next_page(self):
804813 "project_id" : self ._query .project ,
805814 "partition_id" : partition_id ,
806815 "read_options" : read_options ,
807- "query" : query_pb ,
816+ "query" : self . _build_protobuf () ,
808817 }
818+ if self ._query ._explain_options :
819+ request ["explain_options" ] = self ._query ._explain_options ._to_dict ()
809820
810821 helpers .set_database_id_to_request (request , self .client .database )
811822
812- response_pb = self .client ._datastore_api .run_query (
813- request = request ,
814- ** kwargs ,
815- )
823+ response_pb = None
816824
817- while (
825+ while response_pb is None or (
818826 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
820828 ):
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
838839
839840 response_pb = self .client ._datastore_api .run_query (
840- request = request ,
841- ** kwargs ,
841+ request = request .copy (), ** kwargs
842842 )
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+ )
843849
844850 entity_pbs = self ._process_query_results (response_pb )
845851 return page_iterator .Page (self , entity_pbs , self .item_to_value )
846852
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+
847880
848881def _pb_from_query (query ):
849882 """Convert a Query instance to the corresponding protobuf.
0 commit comments