diff --git a/google-cloud-datastore/clirr-ignored-differences.xml b/google-cloud-datastore/clirr-ignored-differences.xml index 34395cc01..1620fd752 100644 --- a/google-cloud-datastore/clirr-ignored-differences.xml +++ b/google-cloud-datastore/clirr-ignored-differences.xml @@ -1,16 +1,58 @@ - + - com/google/cloud/datastore/ReadOption$QueryAndReadOptions - * - 8001 + com/google/cloud/datastore/Datastore + com.google.cloud.datastore.QueryResults run(com.google.cloud.datastore.Query, com.google.cloud.datastore.models.ExplainOptions, com.google.cloud.datastore.ReadOption[]) + 7012 - com/google/cloud/datastore/execution/request/AggregationQueryRequestProtoPreparer - *QueryAndReadOptions* - *QueryConfig* + com/google/cloud/datastore/Datastore + com.google.cloud.datastore.AggregationResults runAggregation(com.google.cloud.datastore.AggregationQuery, com.google.cloud.datastore.models.ExplainOptions, com.google.cloud.datastore.ReadOption[]) + 7012 + + + com/google/cloud/datastore/DatastoreReader + com.google.cloud.datastore.AggregationResults runAggregation(com.google.cloud.datastore.AggregationQuery, com.google.cloud.datastore.models.ExplainOptions) + 7012 + + + com/google/cloud/datastore/QueryResults + java.util.Optional getExplainMetrics() + 7012 + + + com/google/cloud/datastore/Transaction + com.google.cloud.datastore.QueryResults run(com.google.cloud.datastore.Query, com.google.cloud.datastore.models.ExplainOptions) + 7012 + + + + + com/google/cloud/datastore/ReadOption$QueryConfig + com.google.cloud.datastore.ReadOption$QueryConfig create(com.google.cloud.datastore.Query, java.util.List) + *com.google.datastore.v1.ExplainOptions* 7005 + + com/google/cloud/datastore/ReadOption$QueryConfig + com.google.cloud.datastore.ReadOption$QueryConfig create(com.google.cloud.datastore.Query) + 7004 + + + com/google/cloud/datastore/execution/AggregationQueryExecutor + com.google.cloud.datastore.AggregationResults execute(com.google.cloud.datastore.AggregationQuery, com.google.cloud.datastore.ReadOption[]) + 7004 + + + com/google/cloud/datastore/execution/AggregationQueryExecutor + java.lang.Object execute(com.google.cloud.datastore.Query, com.google.cloud.datastore.ReadOption[]) + 7004 + + + com/google/cloud/datastore/execution/QueryExecutor + java.lang.Object execute(com.google.cloud.datastore.Query, com.google.cloud.datastore.ReadOption[]) + 7004 + diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/AggregationResults.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/AggregationResults.java index d85e8f8ae..8e40ab8b7 100644 --- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/AggregationResults.java +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/AggregationResults.java @@ -17,11 +17,14 @@ import static com.google.api.client.util.Preconditions.checkNotNull; +import com.google.api.core.BetaApi; import com.google.api.core.InternalApi; import com.google.cloud.Timestamp; +import com.google.cloud.datastore.models.ExplainMetrics; import java.util.Iterator; import java.util.List; import java.util.Objects; +import java.util.Optional; /** * The result of an {@link AggregationQuery} query submission. Contains a List<{@link @@ -34,11 +37,22 @@ public class AggregationResults implements Iterable { private final List aggregationResults; private final Timestamp readTime; - public AggregationResults(List aggregationResults, Timestamp readTime) { + private final ExplainMetrics explainMetrics; + + @BetaApi + public AggregationResults( + List aggregationResults, + Timestamp readTime, + ExplainMetrics explainMetrics) { checkNotNull(aggregationResults, "Aggregation results cannot be null"); checkNotNull(readTime, "readTime cannot be null"); this.aggregationResults = aggregationResults; this.readTime = readTime; + this.explainMetrics = explainMetrics; + } + + public AggregationResults(List aggregationResults, Timestamp readTime) { + this(aggregationResults, readTime, null); } /** Returns {@link Iterator} for underlying List<{@link AggregationResult}>. */ @@ -51,6 +65,14 @@ public int size() { return this.aggregationResults.size(); } + /** + * Returns {@code ExplainMetrics} if {@code ExplainOptions} were enabled. Otherwise, returns null. + */ + @BetaApi + public Optional getExplainMetrics() { + return Optional.ofNullable(this.explainMetrics); + } + @InternalApi public AggregationResult get(int index) { return this.aggregationResults.get(index); @@ -70,11 +92,13 @@ public boolean equals(Object o) { return false; } AggregationResults that = (AggregationResults) o; - return Objects.equals(aggregationResults, that.aggregationResults); + return Objects.equals(aggregationResults, that.aggregationResults) + && Objects.equals(readTime, that.readTime) + && Objects.equals(explainMetrics, that.explainMetrics); } @Override public int hashCode() { - return Objects.hash(aggregationResults); + return Objects.hash(aggregationResults, readTime, explainMetrics); } } diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/Datastore.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/Datastore.java index d78bea9a2..5bd8384a3 100644 --- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/Datastore.java +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/Datastore.java @@ -16,7 +16,9 @@ package com.google.cloud.datastore; +import com.google.api.core.BetaApi; import com.google.cloud.Service; +import com.google.cloud.datastore.models.ExplainOptions; import com.google.datastore.v1.TransactionOptions; import java.util.Iterator; import java.util.List; @@ -462,6 +464,28 @@ interface TransactionCallable { */ QueryResults run(Query query, ReadOption... options); + /** + * Submits a {@link Query} with specified {@link com.google.cloud.datastore.models.ExplainOptions} + * and returns its result. {@link ReadOption}s can be specified if desired. + * + *

Example of running a query to find all entities of one kind. + * + *

{@code
+   * String kind = "my_kind";
+   * StructuredQuery query = Query.newEntityQueryBuilder()
+   *     .setKind(kind)
+   *     .build();
+   * QueryResults results = datastore.run(query, ExplainOptions.newBuilder().setAnalyze(true).build());
+   * }
+ * + * @throws DatastoreException upon failure + */ + @BetaApi + default QueryResults run( + Query query, ExplainOptions explainOptions, ReadOption... options) { + throw new UnsupportedOperationException("Not implemented."); + } + /** * Submits a {@link AggregationQuery} and returns {@link AggregationResults}. {@link ReadOption}s * can be specified if desired. @@ -508,4 +532,33 @@ interface TransactionCallable { default AggregationResults runAggregation(AggregationQuery query, ReadOption... options) { throw new UnsupportedOperationException("Not implemented."); } + + /** + * Submits a {@link AggregationQuery} with specified {@link + * com.google.cloud.datastore.models.ExplainOptions} and returns {@link AggregationResults}. + * {@link ReadOption}s can be specified if desired. + * + *

Example of running an {@link AggregationQuery} to find the count of entities of one kind. + * + *

{@link StructuredQuery} example: + * + *

{@code
+   * EntityQuery selectAllQuery = Query.newEntityQueryBuilder()
+   *    .setKind("Task")
+   *    .build();
+   * AggregationQuery aggregationQuery = Query.newAggregationQueryBuilder()
+   *    .addAggregation(count().as("total_count"))
+   *    .over(selectAllQuery)
+   *    .build();
+   * AggregationResults aggregationResults = datastore.runAggregation(aggregationQuery, ExplainOptions.newBuilder().setAnalyze(true).build());
+   * }
+ * + * @throws DatastoreException upon failure + * @return {@link AggregationResults} + */ + @BetaApi + default AggregationResults runAggregation( + AggregationQuery query, ExplainOptions explainOptions, ReadOption... options) { + throw new UnsupportedOperationException("Not implemented."); + } } diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/DatastoreImpl.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/DatastoreImpl.java index a1b337c05..a3bfb3796 100644 --- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/DatastoreImpl.java +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/DatastoreImpl.java @@ -16,6 +16,7 @@ package com.google.cloud.datastore; +import com.google.api.core.BetaApi; import com.google.api.gax.retrying.RetrySettings; import com.google.cloud.BaseService; import com.google.cloud.ExceptionHandler; @@ -30,6 +31,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; +import com.google.datastore.v1.ExplainOptions; import com.google.datastore.v1.ReadOptions; import com.google.datastore.v1.ReserveIdsRequest; import com.google.datastore.v1.TransactionOptions; @@ -182,28 +184,54 @@ public T runInTransaction( @Override public QueryResults run(Query query) { - return run(Optional.empty(), query); + return run(Optional.empty(), query, null); } @Override public QueryResults run(Query query, ReadOption... options) { - return run(toReadOptionsPb(options), query); + return run(toReadOptionsPb(options), query, null); + } + + @Override + @BetaApi + public QueryResults run( + Query query, + com.google.cloud.datastore.models.ExplainOptions explainOptions, + ReadOption... options) { + return run(toReadOptionsPb(options), query, explainOptions.toPb()); } @SuppressWarnings("unchecked") - QueryResults run(Optional readOptionsPb, Query query) { + QueryResults run( + Optional readOptionsPb, Query query, ExplainOptions explainOptions) { return new QueryResultsImpl( - this, readOptionsPb, (RecordQuery) query, query.getNamespace()); + this, readOptionsPb, (RecordQuery) query, query.getNamespace(), explainOptions); } @Override public AggregationResults runAggregation(AggregationQuery query) { - return aggregationQueryExecutor.execute(query); + return aggregationQueryExecutor.execute(query, null); } @Override public AggregationResults runAggregation(AggregationQuery query, ReadOption... options) { - return aggregationQueryExecutor.execute(query, options); + return aggregationQueryExecutor.execute(query, null, options); + } + + @Override + @BetaApi + public AggregationResults runAggregation( + AggregationQuery query, com.google.cloud.datastore.models.ExplainOptions explainOptions) { + return aggregationQueryExecutor.execute(query, explainOptions); + } + + @Override + @BetaApi + public AggregationResults runAggregation( + AggregationQuery query, + com.google.cloud.datastore.models.ExplainOptions explainOptions, + ReadOption... options) { + return aggregationQueryExecutor.execute(query, explainOptions, options); } com.google.datastore.v1.RunQueryResponse runQuery( diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/DatastoreReader.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/DatastoreReader.java index 2a3071f3c..8aef7f5c0 100644 --- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/DatastoreReader.java +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/DatastoreReader.java @@ -16,6 +16,8 @@ package com.google.cloud.datastore; +import com.google.api.core.BetaApi; +import com.google.cloud.datastore.models.ExplainOptions; import java.util.Iterator; import java.util.List; @@ -62,4 +64,15 @@ public interface DatastoreReader { default AggregationResults runAggregation(AggregationQuery query) { throw new UnsupportedOperationException("Not implemented."); } + + /** + * Submits a {@link AggregationQuery} with a specified {@link + * com.google.cloud.datastore.models.ExplainOptions} and returns {@link AggregationResults}. + * + * @throws DatastoreException upon failure + */ + @BetaApi + default AggregationResults runAggregation(AggregationQuery query, ExplainOptions explainOptions) { + throw new UnsupportedOperationException("Not implemented."); + } } diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/Query.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/Query.java index 89b8d8807..883051972 100644 --- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/Query.java +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/Query.java @@ -16,6 +16,7 @@ package com.google.cloud.datastore; +import com.google.api.core.InternalApi; import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects.ToStringHelper; import com.google.common.collect.Maps; @@ -117,6 +118,11 @@ public Class resultClass() { return resultClass; } + @InternalApi + public com.google.datastore.v1.EntityResult.ResultType getQueryType() { + return this.queryType; + } + @Override public int hashCode() { return resultClass.hashCode(); diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/QueryResults.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/QueryResults.java index 3f73824dc..50433a6a9 100644 --- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/QueryResults.java +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/QueryResults.java @@ -16,8 +16,11 @@ package com.google.cloud.datastore; +import com.google.api.core.BetaApi; +import com.google.cloud.datastore.models.ExplainMetrics; import com.google.datastore.v1.QueryResultBatch; import java.util.Iterator; +import java.util.Optional; /** * The result of a Google Cloud Datastore query submission. When the result is not typed it is @@ -70,4 +73,9 @@ public interface QueryResults extends Iterator { /** Returns MoreResults state of the query after the current batch. */ QueryResultBatch.MoreResultsType getMoreResults(); + + @BetaApi + default Optional getExplainMetrics() { + throw new UnsupportedOperationException("Not implemented."); + } } diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/QueryResultsImpl.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/QueryResultsImpl.java index 8fc731ace..ad5112989 100644 --- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/QueryResultsImpl.java +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/QueryResultsImpl.java @@ -16,9 +16,12 @@ package com.google.cloud.datastore; +import com.google.api.core.BetaApi; import com.google.cloud.datastore.Query.ResultType; +import com.google.cloud.datastore.models.ExplainMetrics; import com.google.common.base.Preconditions; import com.google.common.collect.AbstractIterator; +import com.google.datastore.v1.ExplainOptions; import com.google.datastore.v1.QueryResultBatch.MoreResultsType; import com.google.datastore.v1.ReadOptions; import com.google.protobuf.ByteString; @@ -40,16 +43,20 @@ class QueryResultsImpl extends AbstractIterator implements QueryResults private Iterator entityResultPbIter; private ByteString cursor; private MoreResultsType moreResults; + private final ExplainOptions explainOptions; + private ExplainMetrics explainMetrics; QueryResultsImpl( DatastoreImpl datastore, Optional readOptionsPb, RecordQuery query, - String namespace) { + String namespace, + ExplainOptions explainOptions) { this.datastore = datastore; this.readOptionsPb = readOptionsPb; this.query = query; queryResultType = query.getType(); + this.explainOptions = explainOptions; com.google.datastore.v1.PartitionId.Builder pbBuilder = com.google.datastore.v1.PartitionId.newBuilder(); pbBuilder.setProjectId(datastore.getOptions().getProjectId()); @@ -75,6 +82,9 @@ private void sendRequest() { requestPb.setPartitionId(partitionIdPb); requestPb.setProjectId(datastore.getOptions().getProjectId()); requestPb.setDatabaseId(datastore.getOptions().getDatabaseId()); + if (explainOptions != null) { + requestPb.setExplainOptions(explainOptions); + } query.populatePb(requestPb); runQueryResponsePb = datastore.runQuery(requestPb.build()); mostRecentQueryPb = requestPb.getQuery(); @@ -86,9 +96,20 @@ private void sendRequest() { // projection entity can represent all type of results actualResultType = ResultType.PROJECTION_ENTITY; } + boolean isExplain = + explainOptions != null + && actualResultType.getQueryType() == null + && !explainOptions.getAnalyze(); Preconditions.checkState( - queryResultType.isAssignableFrom(actualResultType), - "Unexpected result type " + actualResultType + " vs " + queryResultType); + queryResultType.isAssignableFrom(actualResultType) || isExplain, + "Unexpected result type or explain options set " + + actualResultType + + " vs " + + queryResultType + + ", explain options = false"); + if (runQueryResponsePb.hasExplainMetrics()) { + this.explainMetrics = new ExplainMetrics(runQueryResponsePb.getExplainMetrics()); + } } @Override @@ -127,4 +148,10 @@ public int getSkippedResults() { public MoreResultsType getMoreResults() { return moreResults; } + + @Override + @BetaApi + public Optional getExplainMetrics() { + return Optional.ofNullable(this.explainMetrics); + } } diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/ReadOption.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/ReadOption.java index c249e45a6..f7d6d82c7 100644 --- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/ReadOption.java +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/ReadOption.java @@ -20,6 +20,7 @@ import com.google.api.core.InternalApi; import com.google.cloud.Timestamp; import com.google.common.collect.ImmutableMap; +import com.google.datastore.v1.ExplainOptions; import com.google.protobuf.ByteString; import java.io.Serializable; import java.util.Collections; @@ -146,32 +147,38 @@ public static class QueryConfig> { Q query; List readOptions; + ExplainOptions explainOptions; - private QueryConfig(Q query, List readOptions) { + private QueryConfig(Q query, ExplainOptions explainOptions, List readOptions) { this.query = query; + this.explainOptions = explainOptions; this.readOptions = readOptions; } - private QueryConfig(Q query) { - this.query = query; - this.readOptions = Collections.emptyList(); + private QueryConfig(Q query, ExplainOptions explainOptions) { + this(query, explainOptions, Collections.emptyList()); } public Q getQuery() { return query; } + public ExplainOptions getExplainOptions() { + return this.explainOptions; + } + public List getReadOptions() { return readOptions; } - public static > QueryConfig create(Q query) { - return new QueryConfig<>(query); + public static > QueryConfig create( + Q query, ExplainOptions explainOptions) { + return new QueryConfig<>(query, explainOptions); } public static > QueryConfig create( - Q query, List readOptions) { - return new QueryConfig<>(query, readOptions); + Q query, ExplainOptions explainOptions, List readOptions) { + return new QueryConfig<>(query, explainOptions, readOptions); } } } diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/Transaction.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/Transaction.java index 69c18d75c..7b6a67a2d 100644 --- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/Transaction.java +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/Transaction.java @@ -16,6 +16,8 @@ package com.google.cloud.datastore; +import com.google.api.core.BetaApi; +import com.google.cloud.datastore.models.ExplainOptions; import com.google.protobuf.ByteString; import java.util.Iterator; import java.util.List; @@ -176,6 +178,11 @@ interface Response { @Override QueryResults run(Query query); + @BetaApi + default QueryResults run(Query query, ExplainOptions explainOptions) { + throw new UnsupportedOperationException("Not implemented."); + } + /** * Datastore add operation. This method will also allocate id for any entity with an incomplete * key. As opposed to {@link #add(FullEntity)} and {@link #add(FullEntity...)}, this method will diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/TransactionImpl.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/TransactionImpl.java index 3b5e5e4e8..f08a908ec 100644 --- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/TransactionImpl.java +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/TransactionImpl.java @@ -18,6 +18,8 @@ import static com.google.cloud.datastore.ReadOption.transactionId; +import com.google.api.core.BetaApi; +import com.google.cloud.datastore.models.ExplainOptions; import com.google.common.collect.ImmutableList; import com.google.datastore.v1.ReadOptions; import com.google.datastore.v1.TransactionOptions; @@ -102,7 +104,16 @@ public QueryResults run(Query query) { validateActive(); Optional readOptions = this.readOptionProtoPreparer.prepare(ImmutableList.of(transactionId(transactionId))); - return datastore.run(readOptions, query); + return datastore.run(readOptions, query, null); + } + + @Override + @BetaApi + public QueryResults run(Query query, ExplainOptions explainOptions) { + validateActive(); + Optional readOptions = + this.readOptionProtoPreparer.prepare(ImmutableList.of(transactionId(transactionId))); + return datastore.run(readOptions, query, explainOptions.toPb()); } @Override @@ -110,6 +121,12 @@ public AggregationResults runAggregation(AggregationQuery query) { return datastore.runAggregation(query, transactionId(transactionId)); } + @Override + @BetaApi + public AggregationResults runAggregation(AggregationQuery query, ExplainOptions explainOptions) { + return datastore.runAggregation(query, explainOptions, transactionId(transactionId)); + } + @Override public Transaction.Response commit() { validateActive(); diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/execution/AggregationQueryExecutor.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/execution/AggregationQueryExecutor.java index 5a1fdd2c3..bedcf34e4 100644 --- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/execution/AggregationQueryExecutor.java +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/execution/AggregationQueryExecutor.java @@ -23,6 +23,7 @@ import com.google.cloud.datastore.ReadOption.QueryConfig; import com.google.cloud.datastore.execution.request.AggregationQueryRequestProtoPreparer; import com.google.cloud.datastore.execution.response.AggregationQueryResponseTransformer; +import com.google.cloud.datastore.models.ExplainOptions; import com.google.cloud.datastore.spi.v1.DatastoreRpc; import com.google.datastore.v1.RunAggregationQueryRequest; import com.google.datastore.v1.RunAggregationQueryResponse; @@ -47,20 +48,24 @@ public AggregationQueryExecutor(DatastoreRpc datastoreRpc, DatastoreOptions data } @Override - public AggregationResults execute(AggregationQuery query, ReadOption... readOptions) { + public AggregationResults execute( + AggregationQuery query, ExplainOptions explainOptions, ReadOption... readOptions) { RunAggregationQueryRequest runAggregationQueryRequest = - getRunAggregationQueryRequest(query, readOptions); + getRunAggregationQueryRequest( + query, explainOptions == null ? null : explainOptions.toPb(), readOptions); RunAggregationQueryResponse runAggregationQueryResponse = this.datastoreRpc.runAggregationQuery(runAggregationQueryRequest); return this.responseTransformer.transform(runAggregationQueryResponse); } private RunAggregationQueryRequest getRunAggregationQueryRequest( - AggregationQuery query, ReadOption... readOptions) { + AggregationQuery query, + com.google.datastore.v1.ExplainOptions explainOptions, + ReadOption... readOptions) { QueryConfig queryConfig = readOptions == null - ? QueryConfig.create(query) - : QueryConfig.create(query, Arrays.asList(readOptions)); + ? QueryConfig.create(query, explainOptions) + : QueryConfig.create(query, explainOptions, Arrays.asList(readOptions)); return this.protoPreparer.prepare(queryConfig); } } diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/execution/QueryExecutor.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/execution/QueryExecutor.java index 856c64a02..60c75e0f8 100644 --- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/execution/QueryExecutor.java +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/execution/QueryExecutor.java @@ -18,6 +18,7 @@ import com.google.api.core.InternalApi; import com.google.cloud.datastore.Query; import com.google.cloud.datastore.ReadOption; +import com.google.cloud.datastore.models.ExplainOptions; /** * An internal functional interface whose implementation has the responsibility to execute a {@link @@ -34,7 +35,9 @@ public interface QueryExecutor, OUTPUT> { /** * @param query A {@link Query} to execute. + * @param explainOptions {@link com.google.cloud.datastore.models.ExplainOptions}s to be used when + * executing {@link Query}. * @param readOptions Optional {@link ReadOption}s to be used when executing {@link Query}. */ - OUTPUT execute(INPUT query, ReadOption... readOptions); + OUTPUT execute(INPUT query, ExplainOptions explainOptions, ReadOption... readOptions); } diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/execution/request/AggregationQueryRequestProtoPreparer.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/execution/request/AggregationQueryRequestProtoPreparer.java index 475a47b58..a89ebb11a 100644 --- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/execution/request/AggregationQueryRequestProtoPreparer.java +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/execution/request/AggregationQueryRequestProtoPreparer.java @@ -67,7 +67,9 @@ public RunAggregationQueryRequest prepare(QueryConfig queryCon } else { aggregationQueryRequestBuilder.setAggregationQuery(getAggregationQuery(aggregationQuery)); } - + if (queryConfig.getExplainOptions() != null) { + aggregationQueryRequestBuilder.setExplainOptions(queryConfig.getExplainOptions()); + } Optional readOptionsPb = readOptionProtoPreparer.prepare(readOptions); readOptionsPb.ifPresent(aggregationQueryRequestBuilder::setReadOptions); return aggregationQueryRequestBuilder.build(); diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/execution/response/AggregationQueryResponseTransformer.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/execution/response/AggregationQueryResponseTransformer.java index 8c99fcd41..71f7453cb 100644 --- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/execution/response/AggregationQueryResponseTransformer.java +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/execution/response/AggregationQueryResponseTransformer.java @@ -19,6 +19,7 @@ import com.google.cloud.Timestamp; import com.google.cloud.datastore.AggregationResult; import com.google.cloud.datastore.AggregationResults; +import com.google.cloud.datastore.models.ExplainMetrics; import com.google.datastore.v1.RunAggregationQueryResponse; import com.google.datastore.v1.Value; import java.util.AbstractMap.SimpleEntry; @@ -40,7 +41,11 @@ public AggregationResults transform(RunAggregationQueryResponse response) { response.getBatch().getAggregationResultsList().stream() .map(aggregationResult -> new AggregationResult(transformValues(aggregationResult))) .collect(Collectors.toCollection(LinkedList::new)); - return new AggregationResults(aggregationResults, readTime); + ExplainMetrics explainMetrics = null; + if (response.hasExplainMetrics()) { + explainMetrics = new ExplainMetrics(response.getExplainMetrics()); + } + return new AggregationResults(aggregationResults, readTime, explainMetrics); } private Map> transformValues( diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/models/ExecutionStats.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/models/ExecutionStats.java new file mode 100644 index 000000000..52184a01a --- /dev/null +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/models/ExecutionStats.java @@ -0,0 +1,84 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.datastore.models; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.cloud.Structs; +import com.google.common.base.Objects; +import java.util.Map; +import org.threeten.bp.Duration; + +/** Model class for {@link com.google.datastore.v1.ExecutionStats} */ +@BetaApi +public class ExecutionStats { + private final long resultsReturned; + private final Duration executionDuration; + private final long readOperations; + private final Map debugStats; + + @InternalApi + public ExecutionStats(com.google.datastore.v1.ExecutionStats proto) { + this.resultsReturned = proto.getResultsReturned(); + this.executionDuration = Duration.ofNanos(proto.getExecutionDuration().getNanos()); + this.readOperations = proto.getReadOperations(); + this.debugStats = Structs.asMap(proto.getDebugStats()); + } + + /** + * Returns the total number of results returned, including documents, projections, aggregation + * results, keys. + */ + public long getResultsReturned() { + return resultsReturned; + } + + /** Returns the debugging statistics from the execution of the query. */ + public Map getDebugStats() { + return debugStats; + } + + /** Returns the total time to execute the query in the backend. */ + public Duration getExecutionDuration() { + return executionDuration; + } + + /** Returns the total billable read operations. */ + public long getReadOperations() { + return readOperations; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ExecutionStats)) { + return false; + } + ExecutionStats that = (ExecutionStats) o; + + return Objects.equal(resultsReturned, that.resultsReturned) + && Objects.equal(executionDuration, that.executionDuration) + && Objects.equal(readOperations, that.readOperations) + && Objects.equal(debugStats, that.debugStats); + } + + @Override + public int hashCode() { + return Objects.hashCode(resultsReturned, executionDuration, readOperations, debugStats); + } +} diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/models/ExplainMetrics.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/models/ExplainMetrics.java new file mode 100644 index 000000000..75fd94d61 --- /dev/null +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/models/ExplainMetrics.java @@ -0,0 +1,63 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.datastore.models; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.common.base.Objects; +import java.util.Optional; + +/** Model class for {@link com.google.datastore.v1.ExplainMetrics}. */ +@BetaApi +public class ExplainMetrics { + private final PlanSummary planSummary; + private ExecutionStats executionStats; + + @InternalApi + public ExplainMetrics(com.google.datastore.v1.ExplainMetrics proto) { + if (proto.hasExecutionStats()) { + this.executionStats = new ExecutionStats(proto.getExecutionStats()); + } + this.planSummary = new PlanSummary(proto.getPlanSummary()); + } + + /** Returns the planning phase information for the query. */ + public PlanSummary getPlanSummary() { + return planSummary; + } + + /** + * Returns the aggregated stats from the execution of the query, if present. Only present when + * 'analyze' is set to true for {@link ExplainOptions}. + */ + public Optional getExecutionStats() { + return Optional.ofNullable(executionStats); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ExplainMetrics)) return false; + ExplainMetrics that = (ExplainMetrics) o; + return Objects.equal(planSummary, that.planSummary) + && Objects.equal(executionStats, that.executionStats); + } + + @Override + public int hashCode() { + return Objects.hashCode(planSummary, executionStats); + } +} diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/models/ExplainOptions.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/models/ExplainOptions.java new file mode 100644 index 000000000..f339a8510 --- /dev/null +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/models/ExplainOptions.java @@ -0,0 +1,86 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.datastore.models; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Objects; + +/** + * Model class for {@link com.google.datastore.v1.ExplainOptions}. Contains the explain options for + * the query. Analyze is set to 'false' by default. + */ +@BetaApi +public class ExplainOptions { + private final com.google.datastore.v1.ExplainOptions proto; + + private ExplainOptions(com.google.datastore.v1.ExplainOptions proto) { + this.proto = proto; + } + + public static Builder newBuilder() { + return new Builder(); + } + + /** + * Returns whether analyze is set to true or false. When false (the default), the query will be + * planned, returning only metrics from the planning stages. When true, the query will be planned + * and executed, returning the full query results along with both planning and execution stage + * metrics. + */ + public boolean shouldAnalyze() { + return proto.getAnalyze(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ExplainOptions)) return false; + ExplainOptions that = (ExplainOptions) o; + return proto.equals(that.proto); + } + + @Override + public int hashCode() { + return Objects.hashCode(proto); + } + + @InternalApi + @VisibleForTesting + public com.google.datastore.v1.ExplainOptions toPb() { + return this.proto; + } + + public static class Builder { + private boolean analyze = false; + + /* + * Set 'analyze' to true or false for the explain options. + * When false (the default), the query will be planned, returning only metrics from the planning stages. + * When true, the query will be planned and executed, returning the full query results along with both planning and execution stage metrics. + */ + public Builder setAnalyze(boolean analyze) { + this.analyze = analyze; + return this; + } + + public ExplainOptions build() { + return new ExplainOptions( + com.google.datastore.v1.ExplainOptions.newBuilder().setAnalyze(this.analyze).build()); + } + } +} diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/models/PlanSummary.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/models/PlanSummary.java new file mode 100644 index 000000000..ea9e87bd9 --- /dev/null +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/models/PlanSummary.java @@ -0,0 +1,54 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.datastore.models; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.cloud.Structs; +import com.google.common.base.Objects; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** Model class for {@link com.google.datastore.v1.PlanSummary} */ +@BetaApi +public class PlanSummary { + private final List> indexesUsed; + + @InternalApi + public PlanSummary(com.google.datastore.v1.PlanSummary proto) { + this.indexesUsed = + proto.getIndexesUsedList().stream().map(Structs::asMap).collect(Collectors.toList()); + } + + /** Returns the indexes selected for the query. */ + public List> getIndexesUsed() { + return indexesUsed; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PlanSummary)) return false; + PlanSummary planSummary = (PlanSummary) o; + return Objects.equal(indexesUsed, planSummary.indexesUsed); + } + + @Override + public int hashCode() { + return Objects.hashCode(indexesUsed); + } +} diff --git a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/execution/AggregationQueryExecutorTest.java b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/execution/AggregationQueryExecutorTest.java index f9f23261d..46752ba89 100644 --- a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/execution/AggregationQueryExecutorTest.java +++ b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/execution/AggregationQueryExecutorTest.java @@ -88,7 +88,7 @@ public void shouldExecuteAggregationQuery() { replay(mockRpc); - AggregationResults aggregationResults = queryExecutor.execute(aggregationQuery); + AggregationResults aggregationResults = queryExecutor.execute(aggregationQuery, null); verify(mockRpc); assertThat(aggregationResults) @@ -101,7 +101,8 @@ public void shouldExecuteAggregationQuery() { new AggregationResult( ImmutableMap.of( "count", LongValue.of(509), "property_2", LongValue.of(100)))), - Timestamp.fromProto(runAggregationQueryResponse.getBatch().getReadTime()))); + Timestamp.fromProto(runAggregationQueryResponse.getBatch().getReadTime()), + null)); } @Test @@ -127,7 +128,7 @@ public void shouldExecuteAggregationQueryWithReadOptions() { replay(mockRpc); AggregationResults aggregationResults = - queryExecutor.execute(aggregationQuery, eventualConsistency()); + queryExecutor.execute(aggregationQuery, null, eventualConsistency()); verify(mockRpc); assertThat(aggregationResults) @@ -140,7 +141,8 @@ public void shouldExecuteAggregationQueryWithReadOptions() { new AggregationResult( ImmutableMap.of( "count", LongValue.of(509), "property_2", LongValue.of(100)))), - Timestamp.fromProto(runAggregationQueryResponse.getBatch().getReadTime()))); + Timestamp.fromProto(runAggregationQueryResponse.getBatch().getReadTime()), + null)); } private RunAggregationQueryResponse placeholderAggregationQueryResponse() { diff --git a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/execution/request/AggregationQueryRequestProtoPreparerTest.java b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/execution/request/AggregationQueryRequestProtoPreparerTest.java index 0f22828d9..4aec8f7bb 100644 --- a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/execution/request/AggregationQueryRequestProtoPreparerTest.java +++ b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/execution/request/AggregationQueryRequestProtoPreparerTest.java @@ -40,6 +40,7 @@ import com.google.cloud.datastore.ReadOption; import com.google.cloud.datastore.ReadOption.QueryConfig; import com.google.common.collect.ImmutableMap; +import com.google.datastore.v1.ExplainOptions; import com.google.datastore.v1.RunAggregationQueryRequest; import java.util.HashMap; import org.junit.Test; @@ -91,13 +92,14 @@ public class AggregationQueryRequestProtoPreparerTest { @Test public void shouldPrepareAggregationQueryRequestWithGivenStructuredQuery() { RunAggregationQueryRequest runAggregationQueryRequest = - protoPreparer.prepare(QueryConfig.create(AGGREGATION_OVER_STRUCTURED_QUERY)); + protoPreparer.prepare(QueryConfig.create(AGGREGATION_OVER_STRUCTURED_QUERY, null)); assertThat(runAggregationQueryRequest.getProjectId()).isEqualTo(PROJECT_ID); assertThat(runAggregationQueryRequest.getDatabaseId()).isEqualTo(DATABASE_ID); assertThat(runAggregationQueryRequest.getPartitionId().getProjectId()).isEqualTo(PROJECT_ID); assertThat(runAggregationQueryRequest.getPartitionId().getNamespaceId()).isEqualTo(NAMESPACE); + assertThat(runAggregationQueryRequest.hasExplainOptions()).isFalse(); com.google.datastore.v1.AggregationQuery aggregationQueryProto = runAggregationQueryRequest.getAggregationQuery(); @@ -114,7 +116,7 @@ public void shouldPrepareAggregationQueryRequestWithGivenStructuredQuery() { @Test public void shouldPrepareAggregationQueryRequestWithGivenGqlQuery() { RunAggregationQueryRequest runAggregationQueryRequest = - protoPreparer.prepare(QueryConfig.create(AGGREGATION_OVER_GQL_QUERY)); + protoPreparer.prepare(QueryConfig.create(AGGREGATION_OVER_GQL_QUERY, null)); assertThat(runAggregationQueryRequest.getProjectId()).isEqualTo(PROJECT_ID); assertThat(runAggregationQueryRequest.getDatabaseId()).isEqualTo(DATABASE_ID); @@ -122,6 +124,7 @@ public void shouldPrepareAggregationQueryRequestWithGivenGqlQuery() { assertThat(runAggregationQueryRequest.getPartitionId().getProjectId()).isEqualTo(PROJECT_ID); assertThat(runAggregationQueryRequest.getPartitionId().getDatabaseId()).isEqualTo(DATABASE_ID); assertThat(runAggregationQueryRequest.getPartitionId().getNamespaceId()).isEqualTo(NAMESPACE); + assertThat(runAggregationQueryRequest.hasExplainOptions()).isFalse(); com.google.datastore.v1.GqlQuery gqlQueryProto = runAggregationQueryRequest.getGqlQuery(); @@ -172,17 +175,42 @@ public void shouldPrepareAggregationQueryWithNamespaceFromDatastoreOptions() { Query.newAggregationQueryBuilder().over(COMPLETED_TASK_GQL_QUERY).build(); RunAggregationQueryRequest runAggregationQueryFromStructuredQuery = - protoPreparer.prepare(QueryConfig.create(structuredQueryWithoutNamespace)); + protoPreparer.prepare(QueryConfig.create(structuredQueryWithoutNamespace, null)); RunAggregationQueryRequest runAggregationQueryFromGqlQuery = - protoPreparer.prepare(QueryConfig.create(gqlQueryWithoutNamespace)); + protoPreparer.prepare(QueryConfig.create(gqlQueryWithoutNamespace, null)); assertThat(runAggregationQueryFromStructuredQuery.getPartitionId().getNamespaceId()) .isEqualTo(NAMESPACE); assertThat(runAggregationQueryFromGqlQuery.getPartitionId().getNamespaceId()) .isEqualTo(NAMESPACE); + assertThat(runAggregationQueryFromStructuredQuery.hasExplainOptions()).isEqualTo(false); + } + + @Test + public void shouldPrepareAggregationQueryWithDifferentModes() { + AggregationQuery structuredQueryWithoutNamespace = + Query.newAggregationQueryBuilder() + .addAggregation(count().as("total")) + .over(COMPLETED_TASK_STRUCTURED_QUERY) + .build(); + + ExplainOptions explainOptions = ExplainOptions.newBuilder().build(); + RunAggregationQueryRequest runAggregationQueryFromStructuredQuery = + protoPreparer.prepare(QueryConfig.create(structuredQueryWithoutNamespace, explainOptions)); + assertThat(runAggregationQueryFromStructuredQuery.getPartitionId().getNamespaceId()) + .isEqualTo(NAMESPACE); + assertThat(runAggregationQueryFromStructuredQuery.getExplainOptions()) + .isEqualTo(explainOptions); + + RunAggregationQueryRequest runAggregationQueryFromStructuredQuery2 = + protoPreparer.prepare(QueryConfig.create(structuredQueryWithoutNamespace, explainOptions)); + assertThat(runAggregationQueryFromStructuredQuery2.getPartitionId().getNamespaceId()) + .isEqualTo(NAMESPACE); + assertThat(runAggregationQueryFromStructuredQuery2.getExplainOptions()) + .isEqualTo(explainOptions); } private RunAggregationQueryRequest prepareQuery(AggregationQuery query, ReadOption readOption) { - return protoPreparer.prepare(QueryConfig.create(query, singletonList(readOption))); + return protoPreparer.prepare(QueryConfig.create(query, null, singletonList(readOption))); } } diff --git a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/execution/response/AggregationQueryResponseTransformerTest.java b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/execution/response/AggregationQueryResponseTransformerTest.java index 7ba57223f..9263543db 100644 --- a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/execution/response/AggregationQueryResponseTransformerTest.java +++ b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/execution/response/AggregationQueryResponseTransformerTest.java @@ -22,10 +22,15 @@ import com.google.cloud.Timestamp; import com.google.cloud.datastore.AggregationResult; import com.google.cloud.datastore.AggregationResults; +import com.google.cloud.datastore.models.ExplainMetrics; import com.google.common.collect.ImmutableMap; import com.google.datastore.v1.AggregationResultBatch; +import com.google.datastore.v1.ExecutionStats; +import com.google.datastore.v1.PlanSummary; import com.google.datastore.v1.RunAggregationQueryResponse; import com.google.datastore.v1.Value; +import com.google.protobuf.Duration; +import com.google.protobuf.Struct; import java.util.AbstractMap.SimpleEntry; import java.util.HashMap; import java.util.Map; @@ -76,6 +81,81 @@ public void shouldTransformAggregationQueryResponseWithIntValues() { assertThat(aggregationResults.get(0)).isEqualTo(new AggregationResult(toDomainValues(result1))); assertThat(aggregationResults.get(1)).isEqualTo(new AggregationResult(toDomainValues(result2))); assertThat(aggregationResults.getReadTime()).isEqualTo(readTime); + assertThat(aggregationResults.getExplainMetrics().isPresent()).isFalse(); + } + + @Test + public void shouldTransformAggregationQueryResponseWithIntValuesWithStats() { + Map result1 = + new HashMap<>( + ImmutableMap.of( + "count", intValue(209), + "property_2", intValue(100))); + + Map result2 = + new HashMap<>( + ImmutableMap.of( + "count", intValue(509), + "property_2", intValue((100)))); + Timestamp readTime = Timestamp.now(); + + AggregationResultBatch resultBatch = + AggregationResultBatch.newBuilder() + .addAggregationResults( + com.google.datastore.v1.AggregationResult.newBuilder() + .putAllAggregateProperties(result1) + .build()) + .addAggregationResults( + com.google.datastore.v1.AggregationResult.newBuilder() + .putAllAggregateProperties(result2) + .build()) + .setReadTime(readTime.toProto()) + .build(); + + ExecutionStats executionStats = + ExecutionStats.newBuilder() + .setDebugStats( + Struct.newBuilder() + .putFields( + "field", + com.google.protobuf.Value.newBuilder().setStringValue("val").build()) + .build()) + .setExecutionDuration(Duration.newBuilder().setSeconds(1).build()) + .setReadOperations(1) + .setResultsReturned(2) + .build(); + + PlanSummary planSummary = + PlanSummary.newBuilder() + .addIndexesUsed( + Struct.newBuilder() + .putFields( + "field2", + com.google.protobuf.Value.newBuilder().setStringValue("val2").build()) + .build()) + .build(); + + com.google.datastore.v1.ExplainMetrics explainMetrics = + com.google.datastore.v1.ExplainMetrics.newBuilder() + .setExecutionStats(executionStats) + .setPlanSummary(planSummary) + .build(); + + RunAggregationQueryResponse runAggregationQueryResponse = + RunAggregationQueryResponse.newBuilder() + .setBatch(resultBatch) + .setExplainMetrics(explainMetrics) + .build(); + + AggregationResults aggregationResults = + responseTransformer.transform(runAggregationQueryResponse); + + assertThat(aggregationResults.size()).isEqualTo(2); + assertThat(aggregationResults.get(0)).isEqualTo(new AggregationResult(toDomainValues(result1))); + assertThat(aggregationResults.get(1)).isEqualTo(new AggregationResult(toDomainValues(result2))); + assertThat(aggregationResults.getReadTime()).isEqualTo(readTime); + assertThat(aggregationResults.getExplainMetrics().get()) + .isEqualTo(new ExplainMetrics(explainMetrics)); } @Test diff --git a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/ITDatastoreTest.java b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/ITDatastoreTest.java index 7c68ffe32..35e1e72b8 100644 --- a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/ITDatastoreTest.java +++ b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/ITDatastoreTest.java @@ -31,6 +31,7 @@ import com.google.cloud.Timestamp; import com.google.cloud.Tuple; import com.google.cloud.datastore.AggregationQuery; +import com.google.cloud.datastore.AggregationResults; import com.google.cloud.datastore.Batch; import com.google.cloud.datastore.BooleanValue; import com.google.cloud.datastore.Cursor; @@ -66,9 +67,15 @@ import com.google.cloud.datastore.TimestampValue; import com.google.cloud.datastore.Transaction; import com.google.cloud.datastore.ValueType; +import com.google.cloud.datastore.models.ExecutionStats; +import com.google.cloud.datastore.models.ExplainMetrics; +import com.google.cloud.datastore.models.ExplainOptions; +import com.google.cloud.datastore.models.PlanSummary; import com.google.cloud.datastore.testing.RemoteDatastoreHelper; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Range; +import com.google.common.truth.Truth; import com.google.datastore.v1.TransactionOptions; import com.google.datastore.v1.TransactionOptions.ReadOnly; import java.util.ArrayList; @@ -77,6 +84,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -91,6 +99,7 @@ import org.junit.rules.Timeout; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import org.threeten.bp.Duration; @RunWith(Parameterized.class) public class ITDatastoreTest { @@ -358,6 +367,82 @@ public void orQuery() { assertFalse(results4.hasNext()); } + @Test + public void testQueryProfile() { + Key key = Key.newBuilder(KEY1, KIND2, 2).build(); + Entity entity3 = + Entity.newBuilder(ENTITY1) + .setKey(key) + .remove("str") + .set("name", "Dan") + .setNull("null") + .set("age", 19) + .build(); + datastore.put(entity3); + + // age == 19 || age == 20 + CompositeFilter orFilter = + CompositeFilter.or(PropertyFilter.eq("age", 19), PropertyFilter.eq("age", 20)); + StructuredQuery simpleOrQuery = + Query.newEntityQueryBuilder() + .setNamespace(NAMESPACE) + .setKind(KIND2) + .setFilter(orFilter) + .build(); + QueryResults results = + datastore.run(simpleOrQuery, ExplainOptions.newBuilder().setAnalyze(true).build()); + Truth.assertThat(results.hasNext()).isTrue(); + Truth.assertThat(results.getExplainMetrics().isPresent()).isTrue(); + assertPlanSummary(results.getExplainMetrics().get().getPlanSummary()); + assertExecutionStats(results.getExplainMetrics().get().getExecutionStats().get(), 2, 2, "2"); + + QueryResults results2 = + datastore.run(simpleOrQuery, ExplainOptions.newBuilder().build()); + Truth.assertThat(results2.hasNext()).isFalse(); + assertPlanSummary(results2.getExplainMetrics().get().getPlanSummary()); + Truth.assertThat(results2.getExplainMetrics().get().getExecutionStats().isPresent()).isFalse(); + + QueryResults results3 = datastore.run(simpleOrQuery); + Truth.assertThat(results3.hasNext()).isTrue(); + Truth.assertThat(results3.getExplainMetrics().isPresent()).isFalse(); + + QueryResults results4 = + datastore.run(simpleOrQuery, ExplainOptions.newBuilder().setAnalyze(false).build()); + Truth.assertThat(results4.hasNext()).isFalse(); + assertPlanSummary(results4.getExplainMetrics().get().getPlanSummary()); + Truth.assertThat(results4.getExplainMetrics().get().getExecutionStats().isPresent()).isFalse(); + + AggregationQuery aggregationQuery = + Query.newAggregationQueryBuilder().over(simpleOrQuery).addAggregation(count()).build(); + AggregationResults resultsAggregation = + datastore.runAggregation( + aggregationQuery, ExplainOptions.newBuilder().setAnalyze(true).build()); + + Truth.assertThat(resultsAggregation.size() > 0).isTrue(); + + assertPlanSummary(resultsAggregation.getExplainMetrics().get().getPlanSummary()); + assertExecutionStats( + resultsAggregation.getExplainMetrics().get().getExecutionStats().get(), 1, 1, "2"); + + AggregationQuery aggregationQuery2 = + Query.newAggregationQueryBuilder().over(simpleOrQuery).addAggregation(count()).build(); + AggregationResults resultsAggregation2 = + datastore.runAggregation(aggregationQuery2, ExplainOptions.newBuilder().build()); + + Truth.assertThat(resultsAggregation2.size() > 0).isFalse(); + + assertPlanSummary(resultsAggregation2.getExplainMetrics().get().getPlanSummary()); + Truth.assertThat(resultsAggregation2.getExplainMetrics().get().getExecutionStats().isPresent()) + .isFalse(); + + AggregationQuery aggregationQuery3 = + Query.newAggregationQueryBuilder().over(simpleOrQuery).addAggregation(count()).build(); + AggregationResults resultsAggregation3 = datastore.runAggregation(aggregationQuery3); + + Truth.assertThat(resultsAggregation3.size() > 0).isTrue(); + Truth.assertThat(resultsAggregation3.getExplainMetrics().isPresent()).isFalse(); + } + @Test public void testNewTransactionCommit() { Transaction transaction = datastore.newTransaction(); @@ -468,6 +553,100 @@ public void testTransactionWithQuery() throws Exception { assertThat(onlyOneTransactionIsSuccessful).isTrue(); } + @Test + public void testTransactionExplainOptionsAnalyze() { + StructuredQuery query = + Query.newEntityQueryBuilder() + .setKind(KIND2) + .setFilter(PropertyFilter.hasAncestor(KEY2)) + .setNamespace(NAMESPACE) + .build(); + Transaction baseTransaction = datastore.newTransaction(); + QueryResults baseResults = + baseTransaction.run(query, ExplainOptions.newBuilder().setAnalyze(true).build()); + assertTrue(baseResults.hasNext()); + assertEquals(ENTITY2, baseResults.next()); + assertFalse(baseResults.hasNext()); + + ExplainMetrics explainMetrics = baseResults.getExplainMetrics().get(); + assertPlanSummary(explainMetrics.getPlanSummary()); + assertExecutionStats(explainMetrics.getExecutionStats().get(), 1, 1, "1"); + + baseTransaction.add(ENTITY3); + baseTransaction.commit(); + assertEquals(ENTITY3, datastore.get(KEY3)); + + AggregationQuery aggregationQuery = + Query.newAggregationQueryBuilder().addAggregation(count()).over(query).build(); + + Transaction aggregationTransaction = datastore.newTransaction(); + AggregationResults results = + aggregationTransaction.runAggregation( + aggregationQuery, ExplainOptions.newBuilder().setAnalyze(true).build()); + assertTrue(results.size() > 0); + + assertPlanSummary(results.getExplainMetrics().get().getPlanSummary()); + assertExecutionStats(results.getExplainMetrics().get().getExecutionStats().get(), 1, 1, "1"); + aggregationTransaction.commit(); + } + + @Test + public void testTransactionExplainOptions() { + StructuredQuery query = + Query.newEntityQueryBuilder() + .setKind(KIND2) + .setFilter(PropertyFilter.hasAncestor(KEY2)) + .setNamespace(NAMESPACE) + .build(); + Transaction baseTransaction = datastore.newTransaction(); + QueryResults baseResults = + baseTransaction.run(query, ExplainOptions.newBuilder().build()); + assertFalse(baseResults.hasNext()); + + ExplainMetrics explainMetrics = baseResults.getExplainMetrics().get(); + assertPlanSummary(explainMetrics.getPlanSummary()); + Truth.assertThat(explainMetrics.getExecutionStats().isPresent()).isFalse(); + + AggregationQuery aggregationQuery = + Query.newAggregationQueryBuilder().addAggregation(count()).over(query).build(); + + Transaction aggregationTransaction = datastore.newTransaction(); + AggregationResults results = + aggregationTransaction.runAggregation( + aggregationQuery, ExplainOptions.newBuilder().build()); + assertFalse(results.size() > 0); + + assertPlanSummary(results.getExplainMetrics().get().getPlanSummary()); + assertThat(results.getExplainMetrics().get().getExecutionStats().isPresent()).isFalse(); + } + + private void assertPlanSummary(PlanSummary planSummary) { + List> indexesUsed = planSummary.getIndexesUsed(); + indexesUsed.forEach( + each -> Truth.assertThat(each.keySet()).containsAtLeast("properties", "query_scope")); + } + + private void assertExecutionStats( + ExecutionStats executionStats, + long expectedReadOps, + long expectedResultsReturned, + String expectedIndexEntriesScanned) { + Map debugStats = executionStats.getDebugStats(); + Truth.assertThat(debugStats.keySet()) + .containsAtLeast("billing_details", "documents_scanned", "index_entries_scanned"); + Truth.assertThat(debugStats.get("index_entries_scanned")) + .isEqualTo(expectedIndexEntriesScanned); + + Duration executionDuration = executionStats.getExecutionDuration(); + Truth.assertThat(executionDuration).isIn(Range.greaterThan(Duration.ofMillis(0))); + + long readOperations = executionStats.getReadOperations(); + Truth.assertThat(readOperations).isEqualTo(expectedReadOps); + + long resultsReturned = executionStats.getResultsReturned(); + Truth.assertThat(resultsReturned).isEqualTo(expectedResultsReturned); + } + @Test public void testNewTransactionRollback() { Transaction transaction = datastore.newTransaction(); @@ -1646,10 +1825,13 @@ private void testCountAggregationReadTimeWith(Consumer getOnlyElement(datastore.runAggregation(countAggregationQuery)).getLong("total_count"); assertThat(latestCount).isEqualTo(3L); - Long oldCount = - getOnlyElement(datastore.runAggregation(countAggregationQuery, ReadOption.readTime(now))) - .getLong("total_count"); + ExplainOptions explainOptions = ExplainOptions.newBuilder().setAnalyze(true).build(); + AggregationResults results = + datastore.runAggregation(countAggregationQuery, explainOptions, ReadOption.readTime(now)); + Long oldCount = getOnlyElement(results).getLong("total_count"); assertThat(oldCount).isEqualTo(2L); + assertPlanSummary(results.getExplainMetrics().get().getPlanSummary()); + assertExecutionStats(results.getExplainMetrics().get().getExecutionStats().get(), 1, 1, "2"); } finally { datastore.delete(entity1.getKey(), entity2.getKey(), entity3.getKey()); } diff --git a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/models/ExecutionStatsTest.java b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/models/ExecutionStatsTest.java new file mode 100644 index 000000000..4706e3b86 --- /dev/null +++ b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/models/ExecutionStatsTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.datastore.models; + +import com.google.cloud.Structs; +import com.google.common.testing.EqualsTester; +import com.google.common.truth.Truth; +import com.google.protobuf.Duration; +import com.google.protobuf.Struct; +import com.google.protobuf.Value; +import org.junit.Test; + +public class ExecutionStatsTest { + private final Struct struct = + Struct.newBuilder() + .putFields("key", Value.newBuilder().setStringValue("val").build()) + .build(); + private final Duration duration = Duration.newBuilder().setSeconds(23).build(); + private final com.google.datastore.v1.ExecutionStats proto = + com.google.datastore.v1.ExecutionStats.newBuilder() + .setDebugStats(struct) + .setExecutionDuration(duration) + .setReadOperations(2) + .setResultsReturned(3) + .build(); + ExecutionStats executionStats = new ExecutionStats(proto); + + @Test + public void testModel() { + Truth.assertThat(executionStats.getDebugStats()).isEqualTo(Structs.asMap(struct)); + Truth.assertThat(executionStats.getExecutionDuration()) + .isEqualTo(org.threeten.bp.Duration.ofNanos(duration.getNanos())); + Truth.assertThat(executionStats.getReadOperations()).isEqualTo(2); + Truth.assertThat(executionStats.getResultsReturned()).isEqualTo(3); + } + + @Test + public void testEqualsAndHashcode() { + com.google.datastore.v1.ExecutionStats proto2 = + com.google.datastore.v1.ExecutionStats.newBuilder() + .setDebugStats(struct) + .setExecutionDuration(duration) + .setReadOperations(6) + .setResultsReturned(7) + .build(); + + ExecutionStats executionStats1 = new ExecutionStats(proto); + ExecutionStats executionStats2 = new ExecutionStats(proto2); + ExecutionStats executionStats3 = new ExecutionStats(proto2); + + Truth.assertThat(executionStats1).isNotEqualTo(executionStats2); + Truth.assertThat(executionStats2).isEqualTo(executionStats3); + + EqualsTester equalsTester = new EqualsTester(); + equalsTester.addEqualityGroup(executionStats1, executionStats1).testEquals(); + + Truth.assertThat(executionStats1.hashCode()).isNotEqualTo(executionStats2.hashCode()); + Truth.assertThat(executionStats2.hashCode()).isEqualTo(executionStats3.hashCode()); + Truth.assertThat(executionStats1.hashCode()).isEqualTo(executionStats1.hashCode()); + } +} diff --git a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/models/ExplainOptionsTest.java b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/models/ExplainOptionsTest.java new file mode 100644 index 000000000..f8fb86abf --- /dev/null +++ b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/models/ExplainOptionsTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.datastore.models; + +import com.google.common.testing.EqualsTester; +import com.google.common.truth.Truth; +import org.junit.Test; + +public class ExplainOptionsTest { + @Test + public void testModel() { + com.google.datastore.v1.ExplainOptions builtProto = + com.google.datastore.v1.ExplainOptions.newBuilder().setAnalyze(true).build(); + + com.google.datastore.v1.ExplainOptions defaultProto = + com.google.datastore.v1.ExplainOptions.newBuilder().build(); + + ExplainOptions explainOptionsAnalyzeTrue = ExplainOptions.newBuilder().setAnalyze(true).build(); + ExplainOptions explainOptionsAnalyzeFalse = + ExplainOptions.newBuilder().setAnalyze(false).build(); + ExplainOptions explainOptionsDefault = ExplainOptions.newBuilder().build(); + + Truth.assertThat(explainOptionsAnalyzeTrue.shouldAnalyze()).isTrue(); + Truth.assertThat(explainOptionsAnalyzeTrue.toPb()).isEqualTo(builtProto); + + Truth.assertThat(explainOptionsAnalyzeFalse.shouldAnalyze()).isFalse(); + Truth.assertThat(explainOptionsAnalyzeFalse.toPb()).isEqualTo(defaultProto); + + Truth.assertThat(explainOptionsDefault.shouldAnalyze()).isFalse(); + Truth.assertThat(explainOptionsDefault.toPb()).isNotEqualTo(builtProto); + Truth.assertThat(explainOptionsDefault.toPb()).isEqualTo(defaultProto); + Truth.assertThat(explainOptionsDefault).isEqualTo(explainOptionsAnalyzeFalse); + } + + @Test + public void testEqualsAndHashcode() { + ExplainOptions explainOptions = ExplainOptions.newBuilder().build(); + ExplainOptions explainOptions2 = ExplainOptions.newBuilder().setAnalyze(true).build(); + + Truth.assertThat(explainOptions).isNotEqualTo(explainOptions2); + EqualsTester equalsTester = new EqualsTester(); + equalsTester.addEqualityGroup(explainOptions, explainOptions).testEquals(); + + Truth.assertThat(explainOptions.hashCode()).isNotEqualTo(explainOptions2.hashCode()); + Truth.assertThat(explainOptions.hashCode()).isEqualTo(explainOptions.hashCode()); + } +} diff --git a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/models/PlanSummaryTest.java b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/models/PlanSummaryTest.java new file mode 100644 index 000000000..fe8c0f0d6 --- /dev/null +++ b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/models/PlanSummaryTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.datastore.models; + +import com.google.cloud.Structs; +import com.google.common.collect.Lists; +import com.google.common.testing.EqualsTester; +import com.google.common.truth.Truth; +import com.google.protobuf.Struct; +import com.google.protobuf.Value; +import org.junit.Test; + +public class PlanSummaryTest { + private final Struct struct1 = + Struct.newBuilder() + .putFields("key", Value.newBuilder().setStringValue("val").build()) + .build(); + private final Struct struct2 = + Struct.newBuilder() + .putFields("key2", Value.newBuilder().setStringValue("val2").build()) + .putFields("key3", Value.newBuilder().setStringValue("val3").build()) + .build(); + private final com.google.datastore.v1.PlanSummary proto = + com.google.datastore.v1.PlanSummary.newBuilder() + .addIndexesUsed(struct1) + .addIndexesUsed(struct2) + .build(); + private final PlanSummary planSummary = new PlanSummary(proto); + + @Test + public void testModel() { + Truth.assertThat(planSummary.getIndexesUsed()) + .isEqualTo(Lists.newArrayList(Structs.asMap(struct1), Structs.asMap(struct2))); + Truth.assertThat(planSummary.getIndexesUsed().get(0).get("key")).isEqualTo("val"); + } + + @Test + public void testEqualsAndHashcode() { + com.google.datastore.v1.PlanSummary proto2 = + com.google.datastore.v1.PlanSummary.newBuilder().addIndexesUsed(struct1).build(); + PlanSummary planSummary2 = new PlanSummary(proto2); + EqualsTester equalsTester = new EqualsTester(); + equalsTester.addEqualityGroup(planSummary, planSummary).testEquals(); + + Truth.assertThat(planSummary).isNotEqualTo(planSummary2); + + Truth.assertThat(planSummary.hashCode()).isEqualTo(planSummary.hashCode()); + Truth.assertThat(planSummary.hashCode()).isNotEqualTo(planSummary2.hashCode()); + } +}