diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateDemo.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateDemo.java new file mode 100644 index 000000000..6deec3487 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateDemo.java @@ -0,0 +1,247 @@ +/* + * Copyright 2022 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.firestore; + +import static com.google.cloud.firestore.AggregateField.count; +import static com.google.cloud.firestore.AggregateField.last; +import static com.google.cloud.firestore.AggregateField.max; +import static com.google.cloud.firestore.AggregateField.min; +import static com.google.cloud.firestore.FieldPath.documentId; + +import java.util.HashMap; +import java.util.List; +import java.util.Objects; + +public class AggregateDemo { + + public static void Demo1A_CountOfDocumentsInACollection(Firestore db) throws Exception { + Query query = db.collection("games").document("halo").collection("players"); + AggregateSnapshot snapshot = query.aggregate(count()).get().get(); + assertEqual(snapshot.get(count()), 5_000_000); + } + + public static void Demo1B_LimitNumberOfDocumentsScannedWithLimit(Firestore db) throws Exception { + // Limit the work / documents scanned by restricting underlying query. + Query query = db.collection("games").document("halo").collection("players").limit(1000); + AggregateSnapshot snapshot = query.aggregate(count()).get().get(); + assertEqual(snapshot.get(count()), 1000); + } + + public static void Demo1C_LimitNumberOfDocumentsScannedWithUpTo(Firestore db) throws Exception { + // Limit the work / documents scanned by specifying upTo on the aggregation. + Query query = db.collection("games").document("halo").collection("players"); + AggregateSnapshot snapshot = query.aggregate(count().upTo(1000)).get().get(); + assertEqual(snapshot.get(count()), 1000); + } + + public static void Demo2_GroupBySupport(Firestore db) throws Exception { + Query query = db.collectionGroup("players").whereEqualTo("state", "active"); + GroupByQuerySnapshot snapshot = query.groupBy("game").aggregate(count()).get().get(); + assertEqual(snapshot.size(), 3); + List groups = snapshot.getGroups(); + assertEqual(groups.get(0).getString("game"), "cyber_punk"); + assertEqual(groups.get(0).get(count()), 5); + assertEqual(groups.get(1).getString("game"), "halo"); + assertEqual(groups.get(1).get(count()), 55); + assertEqual(groups.get(2).getString("game"), "mine_craft"); + assertEqual(groups.get(2).get(count()), 5_000_000); + } + + public static void Demo3_FieldRenamingAliasing() { + // Aliasing / renaming of aggregations is not exposed from the API surface. + // I've requested that the proto allow non-aggregate fields to also be + // aliased so that the implementation of the Firestore clients can rename + // both aggregate and non-aggregate fields to guarantee that there is NEVER + // a conflict. + } + + public static void Demo4_LimitTheNumberOfDocumentsScanned(Firestore db) throws Exception { + // This is a duplicate of Demo1B_LimitNumberOfDocumentsScannedWithLimit. + } + + public static void Demo5_LimitAggregationBuckets(Firestore db) throws Exception { + Query query = db.collectionGroup("players"); + GroupByQuerySnapshot snapshot = + query.groupBy("game").groupLimit(1).groupOffset(1).aggregate(count()).get().get(); + assertEqual(snapshot.size(), 1); + GroupSnapshot aggregateSnapshot = snapshot.getGroups().get(0); + assertEqual(aggregateSnapshot.getString("game"), "halo"); + assertEqual(aggregateSnapshot.get(count()), 55); + } + + public static void Demo6_LimitWorkPerAggregationBucket(Firestore db) throws Exception { + Query query = db.collection("games").document("halo").collection("players"); + GroupByQuerySnapshot snapshot = query.groupBy("game").aggregate(count().upTo(50)).get().get(); + assertEqual(snapshot.size(), 3); + List groups = snapshot.getGroups(); + assertEqual(groups.get(0).getString("game"), "cyber_punk"); + assertEqual(groups.get(0).get(count()), 5); + assertEqual(groups.get(1).getString("game"), "halo"); + assertEqual(groups.get(1).get(count()), 50); // count is capped at 50 + assertEqual(groups.get(2).getString("game"), "mine_craft"); + assertEqual(groups.get(2).get(count()), 50); // count is capped at 50 + } + + public static void Demo7_OffsetOnNonGroupByQuery() { + // The API does not provide a way to specify an offset for a non-group-by query. + } + + public static void Demo8_PaginationOverAggregationBuckets(Firestore db) throws Exception { + Query query = db.collectionGroup("players").whereEqualTo("state", "active"); + // .orderBy("game") is implied by the group by + GroupByQuerySnapshot snapshot = + query.groupBy("game").groupStartAfter("cyber_punk").aggregate(count()).get().get(); + assertEqual(snapshot.size(), 2); + List groups = snapshot.getGroups(); + assertEqual(groups.get(0).getString("game"), "halo"); + assertEqual(groups.get(0).get(count()), 55); + assertEqual(groups.get(1).getString("game"), "mine_craft"); + assertEqual(groups.get(1).get(count()), 5_000_000); + } + + public static void Demo9A_ResumeTokens(Firestore db) throws Exception { + Query baseQuery = db.collectionGroup("players").limit(1000).orderBy(documentId()); + long playerCount = 0; + + Query query = baseQuery; + while (true) { + AggregateSnapshot snapshot = query.aggregate(count(), last(documentId())).get().get(); + Long count = snapshot.get(count()); + if (count == null) { + throw new NullPointerException("this should never happen"); + } + + playerCount += count; + if (count < 1000) { + break; + } + + // NOTE: If count==0 then snapshot.getString(last(documentId())) returns null. + String lastDocumentId = snapshot.getString(last(documentId())); + query = baseQuery.startAfter(lastDocumentId); + } + + System.out.println("There are " + playerCount + " players"); + } + + public static void Demo9B_ResumeTokensWithGroupBy(Firestore db) throws Exception { + Query baseQuery = db.collectionGroup("players").limit(1000).orderBy(documentId()); + HashMap countByCountry = new HashMap<>(); + + Query query = baseQuery; + while (true) { + GroupByQuerySnapshot snapshot = + query.groupBy("country").aggregate(count(), last(documentId())).get().get(); + long curTotalCount = 0; + String lastDocumentId = null; + + for (GroupSnapshot group : snapshot.getGroups()) { + String country = group.getString("country"); + Long count = group.get(count()); + if (country == null || count == null) { + throw new NullPointerException("this should never happen"); + } + + if (countByCountry.containsKey(country)) { + countByCountry.put(country, countByCountry.get(country) + count); + } else { + countByCountry.put(country, count); + } + + curTotalCount += count; + + // NOTE: last(documentId()) will be exactly the same for all groups; it just gets repeated + // in each group. + lastDocumentId = group.getString(last(documentId())); + if (lastDocumentId == null) { + if (curTotalCount > 0) { + throw new AssertionError( + "lastDocumentId should only be null if no documents were scanned"); + } + } + } + + if (curTotalCount < 1000) { + break; + } + + query = baseQuery.startAfter(lastDocumentId); + } + + for (String country : countByCountry.keySet()) { + System.out.println(country + " has " + countByCountry.get(country) + " players"); + } + } + + public static void Demo10_Max(Firestore db) throws Exception { + Query query = db.collectionGroup("matches").whereEqualTo("game", "halo").orderBy("user"); + GroupByQuerySnapshot snapshot = query.groupBy("user").aggregate(max("timestamp")).get().get(); + assertEqual(snapshot.size(), 2); + List groups = snapshot.getGroups(); + assertEqual(groups.get(0).getString("user"), "alice"); + assertEqual(groups.get(0).getString(max("timestamp")), "2022-01-06"); + assertEqual(groups.get(1).getString("user"), "bob"); + assertEqual(groups.get(1).getString(max("timestamp")), "2021-12-24"); + } + + public static void Demo11_MultipleAggregations(Firestore db) throws Exception { + Query query = db.collectionGroup("matches").whereEqualTo("game", "halo").orderBy("user"); + GroupByQuerySnapshot snapshot = + query.groupBy("user").aggregate(min("score"), max("score")).get().get(); + assertEqual(snapshot.size(), 2); + List groups = snapshot.getGroups(); + assertEqual(groups.get(0).getString("user"), "alice"); + assertEqual(groups.get(0).getLong(min("score")), 0); + assertEqual(groups.get(0).getLong(max("score")), 500); + assertEqual(groups.get(1).getString("user"), "bob"); + assertEqual(groups.get(1).getLong(min("score")), 50); + assertEqual(groups.get(1).getLong(max("score")), 250); + } + + public static void Demo12_Transaction(Firestore db) throws Exception { + db.runTransaction( + txn -> { + AggregateQuery query = + db.collection("games").document("halo").collection("players").aggregate(count()); + AggregateQuerySnapshot snapshot = txn.get(query).get(); + assertEqual(snapshot.get(count()), 5_000_000); + return null; + }); + } + + private static void assertEqual(Long num1, Long num2) { + if (!Objects.equals(num1, num2)) { + throw new AssertionError("num1!=num2"); + } + } + + private static void assertEqual(Long num1, int num2) { + assertEqual(num1, Long.valueOf(num2)); + } + + private static void assertEqual(Integer num1, Integer num2) { + if (!Objects.equals(num1, num2)) { + throw new AssertionError("num1!=num2"); + } + } + + private static void assertEqual(String num1, String num2) { + if (!Objects.equals(num1, num2)) { + throw new AssertionError("num1!=num2"); + } + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateField.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateField.java new file mode 100644 index 000000000..c6f3a63f0 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateField.java @@ -0,0 +1,303 @@ +/* + * Copyright 2022 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.firestore; + +import java.util.Objects; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public abstract class AggregateField { + + private AggregateField() {} + + @Nonnull + public static CountAggregateField count() { + return new CountAggregateField(); + } + + @Nonnull + public static MinAggregateField min(@Nonnull String field) { + return min(FieldPath.fromDotSeparatedString(field)); + } + + @Nonnull + public static MinAggregateField min(@Nonnull FieldPath field) { + return new MinAggregateField(field); + } + + @Nonnull + public static MaxAggregateField max(@Nonnull String field) { + return max(FieldPath.fromDotSeparatedString(field)); + } + + @Nonnull + public static MaxAggregateField max(@Nonnull FieldPath field) { + return new MaxAggregateField(field); + } + + @Nonnull + public static AverageAggregateField average(@Nonnull String field) { + return average(FieldPath.fromDotSeparatedString(field)); + } + + @Nonnull + public static AverageAggregateField average(@Nonnull FieldPath field) { + return new AverageAggregateField(field); + } + + @Nonnull + public static SumAggregateField sum(@Nonnull String field) { + return sum(FieldPath.fromDotSeparatedString(field)); + } + + @Nonnull + public static SumAggregateField sum(@Nonnull FieldPath field) { + return new SumAggregateField(field); + } + + @Nonnull + public static FirstAggregateField first(@Nonnull String field) { + return first(FieldPath.fromDotSeparatedString(field)); + } + + @Nonnull + public static FirstAggregateField first(@Nonnull FieldPath field) { + return new FirstAggregateField(field); + } + + @Nonnull + public static LastAggregateField last(@Nonnull String field) { + return last(FieldPath.fromDotSeparatedString(field)); + } + + @Nonnull + public static LastAggregateField last(@Nonnull FieldPath field) { + return new LastAggregateField(field); + } + + @Override + public abstract boolean equals(Object obj); + + @Override + public abstract int hashCode(); + + @Override + public abstract String toString(); + + public static final class CountAggregateField extends AggregateField { + + @Nullable private Integer upTo; + + CountAggregateField() {} + + CountAggregateField(@Nullable Integer upTo) { + this.upTo = upTo; + } + + public CountAggregateField upTo(int upTo) { + if (upTo < 0) { + throw new IllegalArgumentException("upTo==" + upTo); + } + return new CountAggregateField(upTo); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + CountAggregateField other = (CountAggregateField) obj; + return Objects.equals(upTo, other.upTo); + } + + @Override + public int hashCode() { + return Objects.hash("COUNT", upTo); + } + + @Override + public String toString() { + if (upTo == null) { + return "COUNT"; + } else { + return "COUNT(upTo=" + upTo + ")"; + } + } + } + + public static final class MinAggregateField extends AggregateField { + + @Nonnull private FieldPath field; + + MinAggregateField(@Nonnull FieldPath field) { + this.field = field; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + return field.equals(((MinAggregateField) obj).field); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + @Override + public String toString() { + return "MIN(" + field.toString() + ")"; + } + } + + public static final class MaxAggregateField extends AggregateField { + + @Nonnull private FieldPath field; + + MaxAggregateField(@Nonnull FieldPath field) { + this.field = field; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + return field.equals(((MaxAggregateField) obj).field); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + @Override + public String toString() { + return "MAX(" + field.toString() + ")"; + } + } + + public static final class AverageAggregateField extends AggregateField { + + @Nonnull private FieldPath field; + + AverageAggregateField(@Nonnull FieldPath field) { + this.field = field; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + return field.equals(((AverageAggregateField) obj).field); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + @Override + public String toString() { + return "AVERAGE(" + field.toString() + ")"; + } + } + + public static final class SumAggregateField extends AggregateField { + + @Nonnull private FieldPath field; + + SumAggregateField(@Nonnull FieldPath field) { + this.field = field; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + return field.equals(((SumAggregateField) obj).field); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + @Override + public String toString() { + return "SUM(" + field.toString() + ")"; + } + } + + public static final class FirstAggregateField extends AggregateField { + + @Nonnull private FieldPath field; + + FirstAggregateField(@Nonnull FieldPath field) { + this.field = field; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + return field.equals(((FirstAggregateField) obj).field); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + @Override + public String toString() { + return "FIRST(" + field.toString() + ")"; + } + } + + public static final class LastAggregateField extends AggregateField { + + @Nonnull private FieldPath field; + + LastAggregateField(@Nonnull FieldPath field) { + this.field = field; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + return field.equals(((LastAggregateField) obj).field); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + @Override + public String toString() { + return "LAST(" + field.toString() + ")"; + } + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuery.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuery.java index e99dce925..1e800a184 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuery.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuery.java @@ -32,6 +32,7 @@ import com.google.firestore.v1.Value; import com.google.protobuf.ByteString; import java.util.Set; +import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -246,6 +247,18 @@ public static AggregateQuery fromProto(Firestore firestore, RunAggregationQueryR return new AggregateQuery(query); } + @Nonnull + ListenerRegistration addSnapshotListener( + @Nonnull EventListener listener) { + throw new RuntimeException("not implemented"); + } + + @Nonnull + ListenerRegistration addSnapshotListener( + @Nonnull Executor executor, @Nonnull EventListener listener) { + throw new RuntimeException("not implemented"); + } + /** * Calculates and returns the hash code for this object. * diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuerySnapshot.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuerySnapshot.java index 5b1a17119..bcf7cf257 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuerySnapshot.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuerySnapshot.java @@ -23,16 +23,13 @@ /** The results of executing an {@link AggregateQuery}. */ @InternalExtensionOnly -public class AggregateQuerySnapshot { +public class AggregateQuerySnapshot extends AggregateSnapshot { @Nonnull private final AggregateQuery query; - @Nonnull private final Timestamp readTime; - private final long count; AggregateQuerySnapshot(@Nonnull AggregateQuery query, @Nonnull Timestamp readTime, long count) { + super(readTime, count); this.query = query; - this.readTime = readTime; - this.count = count; } /** Returns the query that was executed to produce this result. */ @@ -41,17 +38,6 @@ public AggregateQuery getQuery() { return query; } - /** Returns the time at which this snapshot was read. */ - @Nonnull - public Timestamp getReadTime() { - return readTime; - } - - /** Returns the number of documents in the result set of the underlying query. */ - public long getCount() { - return count; - } - /** * Compares this object with the given object for equality. * @@ -70,16 +56,13 @@ public long getCount() { */ @Override public boolean equals(Object object) { - if (object == this) { - return true; - } else if (!(object instanceof AggregateQuerySnapshot)) { + if (!super.equals(object)) { return false; } - - AggregateQuerySnapshot other = (AggregateQuerySnapshot) object; - - // Don't check `readTime`, because `DocumentSnapshot.equals()` doesn't either. - return query.equals(other.query) && count == other.count; + if (!(object instanceof AggregateQuerySnapshot)) { + return false; + } + return query.equals(((AggregateQuerySnapshot) object).query); } /** @@ -89,6 +72,6 @@ public boolean equals(Object object) { */ @Override public int hashCode() { - return Objects.hash(query, count); + return Objects.hash(super.hashCode(), query); } } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateSnapshot.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateSnapshot.java new file mode 100644 index 000000000..70ce3006a --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateSnapshot.java @@ -0,0 +1,147 @@ +/* + * Copyright 2022 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.firestore; + +import com.google.api.core.InternalExtensionOnly; +import com.google.cloud.Timestamp; +import java.util.Date; +import java.util.Map; +import java.util.Objects; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +@InternalExtensionOnly +public class AggregateSnapshot { + + @Nonnull private final Timestamp readTime; + private final long count; + + AggregateSnapshot(@Nonnull Timestamp readTime, long count) { + this.readTime = readTime; + this.count = count; + } + + @Nonnull + public Timestamp getReadTime() { + return readTime; + } + + @Nonnull + public Map getAggregations() { + throw new RuntimeException("not implemented"); + } + + public boolean contains(@Nonnull AggregateField field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public Object get(@Nonnull AggregateField field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public T get(@Nonnull AggregateField field, @Nonnull Class valueType) { + throw new RuntimeException("not implemented"); + } + + /** Returns the number of documents in the result set of the underlying query. */ + public long getCount() { + return count; + } + + // Overload get() specifically for COUNT since it has a well-defined type (i.e. long). + public long get(@Nonnull AggregateField.CountAggregateField field) { + throw new RuntimeException("not implemented"); + } + + // Overload get() specifically for SUM since it has a well-defined type (i.e. double). + @Nullable + public Double get(@Nonnull AggregateField.SumAggregateField field) { + throw new RuntimeException("not implemented"); + } + + // Overload get() specifically for AVERAGE since it has a well-defined type (i.e. double). + @Nullable + public Double get(@Nonnull AggregateField.AverageAggregateField field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public Boolean getBoolean(@Nonnull AggregateField field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public Double getDouble(@Nonnull AggregateField field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public String getString(@Nonnull AggregateField field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public Long getLong(@Nonnull AggregateField field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public Date getDate(@Nonnull AggregateField field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public Timestamp getTimestamp(@Nonnull AggregateField field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public Blob getBlob(@Nonnull AggregateField field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public GeoPoint getGeoPoint(@Nonnull AggregateField field) { + throw new RuntimeException("not implemented"); + } + + @Override + public boolean equals(Object object) { + if (object == this) { + return true; + } else if (!(object instanceof AggregateSnapshot)) { + return false; + } + + AggregateSnapshot other = (AggregateSnapshot) object; + + // Don't check `readTime`, because `DocumentSnapshot.equals()` doesn't either. + return count == other.count; + } + + /** + * Calculates and returns the hash code for this object. + * + * @return the hash code for this object. + */ + @Override + public int hashCode() { + return Objects.hash(count); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/GroupByQuery.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/GroupByQuery.java new file mode 100644 index 000000000..a3ca10729 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/GroupByQuery.java @@ -0,0 +1,107 @@ +/* + * Copyright 2022 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.firestore; + +import com.google.api.core.ApiFuture; +import com.google.api.gax.rpc.ApiStreamObserver; +import com.google.cloud.firestore.Query.Direction; +import java.util.concurrent.Executor; +import javax.annotation.Nonnull; + +public interface GroupByQuery { + + @Nonnull + Query getQuery(); + + @Nonnull + ApiFuture get(); + + void stream(@Nonnull final ApiStreamObserver responseObserver); + + @Nonnull + ListenerRegistration addSnapshotListener(@Nonnull EventListener listener); + + @Nonnull + ListenerRegistration addSnapshotListener( + @Nonnull Executor executor, @Nonnull EventListener listener); + + // Note: Specifying an empty list of aggregates to this method, or not invoking it at all, is + // equivalent to an SQL "DISTINCT" operator. + @Nonnull + GroupByQuery aggregate(@Nonnull AggregateField... fields); + + @Nonnull + GroupByQuery groupLimit(int maxGroups); + + // Question: Do we want to support group-by "limitToLast" queries? In the Query class this is + // implemented entirely client side by issuing the requested query with inverted order-by. We + // would need to verify at runtime that the underlying query has the correct order-by clause and + // possibly invert first/last aggregations to maintain their expected semantics. + @Nonnull + GroupByQuery groupLimitToLast(int maxGroups); + + @Nonnull + GroupByQuery groupOffset(long groupOffset); + + @Nonnull + GroupByQuery groupStartAt(Object... fieldValues); + + @Nonnull + GroupByQuery groupStartAt(@Nonnull GroupSnapshot snapshot); + + @Nonnull + GroupByQuery groupStartAfter(Object... fieldValues); + + @Nonnull + GroupByQuery groupStartAfter(@Nonnull GroupSnapshot snapshot); + + @Nonnull + GroupByQuery groupEndAt(Object... fieldValues); + + @Nonnull + GroupByQuery groupEndAt(@Nonnull GroupSnapshot snapshot); + + @Nonnull + GroupByQuery groupEndBefore(Object... fieldValues); + + @Nonnull + GroupByQuery groupEndBefore(@Nonnull GroupSnapshot snapshot); + + @Nonnull + GroupByQuery groupOrderBy(@Nonnull String groupByField); + + @Nonnull + GroupByQuery groupOrderBy(@Nonnull FieldPath groupByField); + + @Nonnull + GroupByQuery groupOrderBy(@Nonnull AggregateField aggregateField); + + @Nonnull + GroupByQuery groupOrderBy(@Nonnull String groupByField, @Nonnull Direction direction); + + @Nonnull + GroupByQuery groupOrderBy(@Nonnull FieldPath groupByField, @Nonnull Direction direction); + + @Nonnull + GroupByQuery groupOrderBy(@Nonnull AggregateField aggregateField, @Nonnull Direction direction); + + @Override + int hashCode(); + + @Override + boolean equals(Object obj); +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/GroupByQuerySnapshot.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/GroupByQuerySnapshot.java new file mode 100644 index 000000000..a57d7dc34 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/GroupByQuerySnapshot.java @@ -0,0 +1,46 @@ +/* + * Copyright 2022 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.firestore; + +import com.google.cloud.Timestamp; +import java.util.List; +import javax.annotation.Nonnull; + +public interface GroupByQuerySnapshot extends Iterable { + + @Nonnull + GroupByQuery getQuery(); + + @Nonnull + Timestamp getReadTime(); + + @Nonnull + List getGroups(); + + @Nonnull + List getGroupChanges(); + + boolean isEmpty(); + + int size(); + + @Override + boolean equals(Object obj); + + @Override + int hashCode(); +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/GroupChange.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/GroupChange.java new file mode 100644 index 000000000..c9c7db8d6 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/GroupChange.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017 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.firestore; + +import javax.annotation.Nonnull; + +public interface GroupChange { + + enum Type { + ADDED, + MODIFIED, + REMOVED + } + + @Nonnull + Type getType(); + + @Nonnull + GroupSnapshot getGroup(); + + int getOldIndex(); + + int getNewIndex(); + + @Override + boolean equals(Object obj); + + @Override + int hashCode(); +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/GroupSnapshot.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/GroupSnapshot.java new file mode 100644 index 000000000..1699a80a4 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/GroupSnapshot.java @@ -0,0 +1,145 @@ +/* + * Copyright 2022 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.firestore; + +import com.google.api.core.InternalExtensionOnly; +import com.google.cloud.Timestamp; +import java.util.Date; +import java.util.Map; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +@InternalExtensionOnly +public class GroupSnapshot extends AggregateSnapshot { + + GroupSnapshot(@Nonnull Timestamp readTime, long count) { + super(readTime, count); + } + + @Nonnull + public Map getFields() { + throw new RuntimeException("not implemented"); + } + + public boolean contains(@Nonnull String field) { + throw new RuntimeException("not implemented"); + } + + public boolean contains(@Nonnull FieldPath field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public Object get(@Nonnull String field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public T get(@Nonnull String field, @Nonnull Class valueType) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public Object get(@Nonnull FieldPath field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public T get(@Nonnull FieldPath field, @Nonnull Class valueType) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public Boolean getBoolean(@Nonnull String field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public Boolean getBoolean(@Nonnull FieldPath field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public Double getDouble(@Nonnull String field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public Double getDouble(@Nonnull FieldPath field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public String getString(@Nonnull String field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public String getString(@Nonnull FieldPath field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public Long getLong(@Nonnull String field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public Long getLong(@Nonnull FieldPath field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public Date getDate(@Nonnull String field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public Date getDate(@Nonnull FieldPath field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public Timestamp getTimestamp(@Nonnull String field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public Timestamp getTimestamp(@Nonnull FieldPath field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public Blob getBlob(@Nonnull String field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public Blob getBlob(@Nonnull FieldPath field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public GeoPoint getGeoPoint(@Nonnull String field) { + throw new RuntimeException("not implemented"); + } + + @Nullable + public GeoPoint getGeoPoint(@Nonnull FieldPath field) { + throw new RuntimeException("not implemented"); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java index 931a98131..9cc6c938e 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java @@ -1872,6 +1872,20 @@ public AggregateQuery count() { return new AggregateQuery(this); } + @Nonnull + public AggregateQuery aggregate( + @Nonnull AggregateField field, @Nonnull AggregateField... fields) { + throw new RuntimeException("not implemented"); + } + + public GroupByQuery groupBy(@Nonnull String field1, @Nonnull String... fields) { + throw new RuntimeException("not implemented"); + } + + public GroupByQuery groupBy(@Nonnull FieldPath field1, @Nonnull FieldPath... fields) { + throw new RuntimeException("not implemented"); + } + /** * Returns true if this Query is equal to the provided object. * diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Transaction.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Transaction.java index 3ca949373..5b267ebbc 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Transaction.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Transaction.java @@ -203,4 +203,9 @@ public ApiFuture get(@Nonnull AggregateQuery query) { return query.get(transactionId); } + + @Nonnull + public ApiFuture get(@Nonnull GroupByQuery query) { + throw new RuntimeException("not implemented"); + } }