diff --git a/.gitignore b/.gitignore index 609430f4ea..0ad1525e37 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,7 @@ gcs_key_file.json # Folders for cmake/test output *_build/ -*cmake-build-debug/ +cmake-build-*/ testing/test_framework/external/ # XCode user specific folders diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index eae3591294..5db0a3f90c 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -523,6 +523,9 @@ if (IOS) ${FIREBASE_SOURCE_DIR}/dynamic_links/src/include/firebase/dynamic_links/components.h) set(firestore_HDRS ${FIREBASE_SOURCE_DIR}/firestore/src/include/firebase/firestore.h + ${FIREBASE_SOURCE_DIR}/firestore/src/include/firebase/firestore/aggregate_query.h + ${FIREBASE_SOURCE_DIR}/firestore/src/include/firebase/firestore/aggregate_query_snapshot.h + ${FIREBASE_SOURCE_DIR}/firestore/src/include/firebase/firestore/aggregate_source.h ${FIREBASE_SOURCE_DIR}/firestore/src/include/firebase/firestore/collection_reference.h ${FIREBASE_SOURCE_DIR}/firestore/src/include/firebase/firestore/document_change.h ${FIREBASE_SOURCE_DIR}/firestore/src/include/firebase/firestore/document_reference.h diff --git a/firestore/CMakeLists.txt b/firestore/CMakeLists.txt index dc9b1219e9..762d217554 100644 --- a/firestore/CMakeLists.txt +++ b/firestore/CMakeLists.txt @@ -20,6 +20,8 @@ else() endif() set(common_SRCS + src/common/aggregate_query.cc + src/common/aggregate_query_snapshot.cc src/common/cleanup.h src/common/collection_reference.cc src/common/compiler_info.cc @@ -65,6 +67,12 @@ binary_to_array("firestore_resources" set(android_SRCS ${firestore_resources_source} + src/android/aggregate_query_android.cc + src/android/aggregate_query_android.h + src/android/aggregate_query_snapshot_android.cc + src/android/aggregate_query_snapshot_android.h + src/android/aggregate_source_android.cc + src/android/aggregate_source_android.h src/android/blob_android.cc src/android/blob_android.h src/android/collection_reference_android.cc @@ -185,6 +193,10 @@ set(android_SRCS # Sources that apply to all non-Android platforms. set(main_SRCS + src/main/aggregate_query_main.cc + src/main/aggregate_query_main.h + src/main/aggregate_query_snapshot_main.cc + src/main/aggregate_query_snapshot_main.h src/main/collection_reference_main.cc src/main/collection_reference_main.h src/main/converter_main.h diff --git a/firestore/CONTRIBUTING.md b/firestore/CONTRIBUTING.md index 51cc290734..d7ad3455ea 100644 --- a/firestore/CONTRIBUTING.md +++ b/firestore/CONTRIBUTING.md @@ -19,7 +19,7 @@ a Python3 installation. # Architecture -It is easier to work this Firestore CPP SDK by keeping a high level archetecture in mind: +It is easier to work this Firestore CPP SDK by keeping a high level architecture in mind: ![architecture.png](architecture.png) diff --git a/firestore/integration_test_internal/CMakeLists.txt b/firestore/integration_test_internal/CMakeLists.txt index 0205e5b164..f6e22bd8b1 100644 --- a/firestore/integration_test_internal/CMakeLists.txt +++ b/firestore/integration_test_internal/CMakeLists.txt @@ -91,6 +91,8 @@ set(FIREBASE_INTEGRATION_TEST_PORTABLE_TEST_SRCS # public API are performed. src/integration_test.cc # Internal tests below. + src/aggregate_query_snapshot_test.cc + src/aggregate_query_test.cc src/bundle_test.cc src/collection_reference_test.cc src/cursor_test.cc diff --git a/firestore/integration_test_internal/src/aggregate_count_test.cc b/firestore/integration_test_internal/src/aggregate_count_test.cc new file mode 100644 index 0000000000..dc58961fc3 --- /dev/null +++ b/firestore/integration_test_internal/src/aggregate_count_test.cc @@ -0,0 +1,771 @@ +/* + * Copyright 2023 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. + */ + +#include +#include +#include +#include +#include +#include + +#include "firebase/firestore.h" +#include "firebase/firestore/field_value.h" +#include "firebase/firestore/map_field_value.h" +#include "firestore/src/common/macros.h" +#include "firestore_integration_test.h" +#include "util/event_accumulator.h" + +#if defined(__ANDROID__) +#include "firestore/src/android/query_android.h" +#include "firestore/src/common/wrapper_assertions.h" +#endif // defined(__ANDROID__) + +#include "Firestore/core/src/util/firestore_exceptions.h" +#include "firebase/firestore/firestore_errors.h" +#include "firebase_test_framework.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace { + +using AggregateCountTest = FirestoreIntegrationTest; + +} // namespace + +TEST_F(AggregateCountTest, TestKeyOrderIsDescendingForDescendingInequality) { + CollectionReference collection = + Collection({{"a", {{"foo", FieldValue::Integer(42)}}}, + {"b", {{"foo", FieldValue::Double(42.0)}}}, + {"c", {{"foo", FieldValue::Integer(42)}}}, + {"d", {{"foo", FieldValue::Integer(21)}}}, + {"e", {{"foo", FieldValue::Double(21.0)}}}, + {"f", {{"foo", FieldValue::Integer(66)}}}, + {"g", {{"foo", FieldValue::Double(66.0)}}}}); + const AggregateQuery aggregate_query = + collection.WhereGreaterThan("foo", FieldValue::Integer(21)) + .OrderBy(FieldPath({"foo"}), Query::Direction::kDescending) + .Count(); + const AggregateQuerySnapshot aggregate_snapshot = + ReadAggregate(aggregate_query); + EXPECT_EQ(5, aggregate_snapshot.count()); + EXPECT_EQ(aggregate_query, aggregate_snapshot.query()); +} + +TEST_F(AggregateCountTest, TestUnaryFilterQueries) { + CollectionReference collection = Collection( + {{"a", {{"null", FieldValue::Null()}, {"nan", FieldValue::Double(NAN)}}}, + {"b", {{"null", FieldValue::Null()}, {"nan", FieldValue::Integer(0)}}}, + {"c", + {{"null", FieldValue::Boolean(false)}, + {"nan", FieldValue::Double(NAN)}}}}); + + const AggregateQuery aggregate_query = + collection.WhereEqualTo("null", FieldValue::Null()) + .WhereEqualTo("nan", FieldValue::Double(NAN)) + .Count(); + const AggregateQuerySnapshot aggregate_snapshot = + ReadAggregate(aggregate_query); + EXPECT_EQ(1, aggregate_snapshot.count()); + EXPECT_EQ(aggregate_query, aggregate_snapshot.query()); +} + +TEST_F(AggregateCountTest, TestQueryWithFieldPaths) { + CollectionReference collection = + Collection({{"a", {{"a", FieldValue::Integer(1)}}}, + {"b", {{"a", FieldValue::Integer(2)}}}, + {"c", {{"a", FieldValue::Integer(3)}}}}); + const AggregateQuery aggregate_query = + collection.WhereLessThan(FieldPath({"a"}), FieldValue::Integer(3)) + .OrderBy(FieldPath({"a"}), Query::Direction::kDescending) + .Count(); + const AggregateQuerySnapshot aggregate_snapshot = + ReadAggregate(aggregate_query); + EXPECT_EQ(2, aggregate_snapshot.count()); + EXPECT_EQ(aggregate_query, aggregate_snapshot.query()); +} + +TEST_F(AggregateCountTest, TestFilterOnInfinity) { + CollectionReference collection = + Collection({{"a", {{"inf", FieldValue::Double(INFINITY)}}}, + {"b", {{"inf", FieldValue::Double(-INFINITY)}}}}); + + const AggregateQuery aggregate_query = + collection.WhereEqualTo("inf", FieldValue::Double(INFINITY)).Count(); + const AggregateQuerySnapshot aggregate_snapshot = + ReadAggregate(aggregate_query); + EXPECT_EQ(1, aggregate_snapshot.count()); + EXPECT_EQ(aggregate_query, aggregate_snapshot.query()); +} + +TEST_F(AggregateCountTest, TestCanQueryByDocumentId) { + CollectionReference collection = + Collection({{"aa", {{"key", FieldValue::String("aa")}}}, + {"ab", {{"key", FieldValue::String("ab")}}}, + {"ba", {{"key", FieldValue::String("ba")}}}, + {"bb", {{"key", FieldValue::String("bb")}}}}); + + // Query by Document Id. + const AggregateQuery aggregate_query1 = + collection.WhereEqualTo(FieldPath::DocumentId(), FieldValue::String("ab")) + .Count(); + const AggregateQuerySnapshot aggregate_snapshot1 = + ReadAggregate(aggregate_query1); + EXPECT_EQ(1, aggregate_snapshot1.count()); + EXPECT_EQ(aggregate_query1, aggregate_snapshot1.query()); + + // Query by Document Ids. + const AggregateQuery aggregate_query2 = + collection + .WhereGreaterThan(FieldPath::DocumentId(), FieldValue::String("aa")) + .WhereLessThanOrEqualTo(FieldPath::DocumentId(), + FieldValue::String("ba")) + .Count(); + const AggregateQuerySnapshot aggregate_snapshot2 = + ReadAggregate(aggregate_query2); + EXPECT_EQ(2, aggregate_snapshot2.count()); + EXPECT_EQ(aggregate_query2, aggregate_snapshot2.query()); +} + +TEST_F(AggregateCountTest, TestCanQueryByDocumentIdUsingRefs) { + CollectionReference collection = + Collection({{"aa", {{"key", FieldValue::String("aa")}}}, + {"ab", {{"key", FieldValue::String("ab")}}}, + {"ba", {{"key", FieldValue::String("ba")}}}, + {"bb", {{"key", FieldValue::String("bb")}}}}); + + // Query by Document Id. + const AggregateQuery aggregate_query1 = + collection + .WhereEqualTo(FieldPath::DocumentId(), + FieldValue::Reference(collection.Document("ab"))) + .Count(); + const AggregateQuerySnapshot aggregate_snapshot1 = + ReadAggregate(aggregate_query1); + EXPECT_EQ(1, aggregate_snapshot1.count()); + EXPECT_EQ(aggregate_query1, aggregate_snapshot1.query()); + + // Query by Document Ids. + const AggregateQuery aggregate_query2 = + collection + .WhereGreaterThan(FieldPath::DocumentId(), + FieldValue::Reference(collection.Document("aa"))) + .WhereLessThanOrEqualTo( + FieldPath::DocumentId(), + FieldValue::Reference(collection.Document("ba"))) + .Count(); + const AggregateQuerySnapshot aggregate_snapshot2 = + ReadAggregate(aggregate_query2); + EXPECT_EQ(2, aggregate_snapshot2.count()); + EXPECT_EQ(aggregate_query2, aggregate_snapshot2.query()); +} + +TEST_F(AggregateCountTest, TestQueriesCanUseNotEqualFilters) { + // These documents are ordered by value in "zip" since the NotEqual filter is + // an inequality, which results in documents being sorted by value. + std::map docs = { + {"a", {{"zip", FieldValue::Double(NAN)}}}, + {"b", {{"zip", FieldValue::Integer(91102)}}}, + {"c", {{"zip", FieldValue::Integer(98101)}}}, + {"d", {{"zip", FieldValue::String("98101")}}}, + {"e", {{"zip", FieldValue::Array({FieldValue::Integer(98101)})}}}, + {"f", + {{"zip", FieldValue::Array({FieldValue::Integer(98101), + FieldValue::Integer(98102)})}}}, + {"g", + {{"zip", FieldValue::Array({FieldValue::String("98101"), + FieldValue::Map({{"zip", FieldValue::Integer( + 98101)}})})}}}, + {"h", {{"zip", FieldValue::Map({{"code", FieldValue::Integer(500)}})}}}, + {"i", {{"code", FieldValue::Integer(500)}}}, + {"j", MapFieldValue{{"zip", FieldValue::Null()}}}, + }; + CollectionReference collection = Collection(docs); + + // Search for zips not matching 98101. + const AggregateQuery aggregate_query = + collection.WhereNotEqualTo("zip", FieldValue::Integer(98101)).Count(); + const AggregateQuerySnapshot aggregate_snapshot = + ReadAggregate(aggregate_query); + EXPECT_EQ(7, aggregate_snapshot.count()); + EXPECT_EQ(aggregate_query, aggregate_snapshot.query()); +} + +TEST_F(AggregateCountTest, TestQueriesCanUseNotEqualFiltersWithObject) { + // These documents are ordered by value in "zip" since the NotEqual filter is + // an inequality, which results in documents being sorted by value. + std::map docs = { + {"a", {{"zip", FieldValue::Double(NAN)}}}, + {"b", {{"zip", FieldValue::Integer(91102)}}}, + {"c", {{"zip", FieldValue::Integer(98101)}}}, + {"d", {{"zip", FieldValue::String("98101")}}}, + {"e", {{"zip", FieldValue::Array({FieldValue::Integer(98101)})}}}, + {"f", + {{"zip", FieldValue::Array({FieldValue::Integer(98101), + FieldValue::Integer(98102)})}}}, + {"g", + {{"zip", FieldValue::Array({FieldValue::String("98101"), + FieldValue::Map({{"zip", FieldValue::Integer( + 98101)}})})}}}, + {"h", {{"zip", FieldValue::Map({{"code", FieldValue::Integer(500)}})}}}, + {"i", {{"code", FieldValue::Integer(500)}}}, + {"j", MapFieldValue{{"zip", FieldValue::Null()}}}, + }; + CollectionReference collection = Collection(docs); + + const AggregateQuery aggregate_query = + collection + .WhereNotEqualTo( + "zip", FieldValue::Map({{"code", FieldValue::Integer(500)}})) + .Count(); + const AggregateQuerySnapshot aggregate_snapshot = + ReadAggregate(aggregate_query); + EXPECT_EQ(7, aggregate_snapshot.count()); + EXPECT_EQ(aggregate_query, aggregate_snapshot.query()); +} + +TEST_F(AggregateCountTest, TestQueriesCanUseNotEqualFiltersWithNull) { + // These documents are ordered by value in "zip" since the NotEqual filter is + // an inequality, which results in documents being sorted by value. + std::map docs = { + {"a", {{"zip", FieldValue::Double(NAN)}}}, + {"b", {{"zip", FieldValue::Integer(91102)}}}, + {"c", {{"zip", FieldValue::Integer(98101)}}}, + {"d", {{"zip", FieldValue::String("98101")}}}, + {"e", {{"zip", FieldValue::Array({FieldValue::Integer(98101)})}}}, + {"f", + {{"zip", FieldValue::Array({FieldValue::Integer(98101), + FieldValue::Integer(98102)})}}}, + {"g", + {{"zip", FieldValue::Array({FieldValue::String("98101"), + FieldValue::Map({{"zip", FieldValue::Integer( + 98101)}})})}}}, + {"h", {{"zip", FieldValue::Map({{"code", FieldValue::Integer(500)}})}}}, + {"i", {{"code", FieldValue::Integer(500)}}}, + {"j", MapFieldValue{{"zip", FieldValue::Null()}}}, + }; + CollectionReference collection = Collection(docs); + + // With Null. + const AggregateQuery aggregate_query = + collection + .WhereNotEqualTo("zip", + FieldValue::Map({{"code", FieldValue::Null()}})) + .Count(); + const AggregateQuerySnapshot aggregate_snapshot = + ReadAggregate(aggregate_query); + EXPECT_EQ(8, aggregate_snapshot.count()); + EXPECT_EQ(aggregate_query, aggregate_snapshot.query()); +} + +TEST_F(AggregateCountTest, TestQueriesCanUseNotEqualFiltersWithNan) { + // These documents are ordered by value in "zip" since the NotEqual filter is + // an inequality, which results in documents being sorted by value. + std::map docs = { + {"a", {{"zip", FieldValue::Double(NAN)}}}, + {"b", {{"zip", FieldValue::Integer(91102)}}}, + {"c", {{"zip", FieldValue::Integer(98101)}}}, + {"d", {{"zip", FieldValue::String("98101")}}}, + {"e", {{"zip", FieldValue::Array({FieldValue::Integer(98101)})}}}, + {"f", + {{"zip", FieldValue::Array({FieldValue::Integer(98101), + FieldValue::Integer(98102)})}}}, + {"g", + {{"zip", FieldValue::Array({FieldValue::String("98101"), + FieldValue::Map({{"zip", FieldValue::Integer( + 98101)}})})}}}, + {"h", {{"zip", FieldValue::Map({{"code", FieldValue::Integer(500)}})}}}, + {"i", {{"code", FieldValue::Integer(500)}}}, + {"j", MapFieldValue{{"zip", FieldValue::Null()}}}, + }; + CollectionReference collection = Collection(docs); + + const AggregateQuery aggregate_query = + collection.WhereNotEqualTo("zip", FieldValue::Double(NAN)).Count(); + const AggregateQuerySnapshot aggregate_snapshot = + ReadAggregate(aggregate_query); + EXPECT_EQ(7, aggregate_snapshot.count()); + EXPECT_EQ(aggregate_query, aggregate_snapshot.query()); +} + +TEST_F(AggregateCountTest, TestQueriesCanUseNotEqualFiltersWithDocIds) { + MapFieldValue doc_a = {{"key", FieldValue::String("aa")}}; + MapFieldValue doc_b = {{"key", FieldValue::String("ab")}}; + MapFieldValue doc_c = {{"key", FieldValue::String("ba")}}; + MapFieldValue doc_d = {{"key", FieldValue::String("bb")}}; + + CollectionReference collection = + Collection({{"aa", doc_a}, {"ab", doc_b}, {"ba", doc_c}, {"bb", doc_d}}); + + const AggregateQuery aggregate_query = + collection + .WhereNotEqualTo(FieldPath::DocumentId(), FieldValue::String("aa")) + .Count(); + const AggregateQuerySnapshot aggregate_snapshot = + ReadAggregate(aggregate_query); + EXPECT_EQ(3, aggregate_snapshot.count()); + EXPECT_EQ(aggregate_query, aggregate_snapshot.query()); +} + +TEST_F(AggregateCountTest, TestQueriesCanUseArrayContainsFilters) { + CollectionReference collection = Collection( + {{"a", {{"array", FieldValue::Array({FieldValue::Integer(42)})}}}, + {"b", + {{"array", + FieldValue::Array({FieldValue::String("a"), FieldValue::Integer(42), + FieldValue::String("c")})}}}, + {"c", + {{"array", + FieldValue::Array( + {FieldValue::Double(41.999), FieldValue::String("42"), + FieldValue::Map( + {{"a", FieldValue::Array({FieldValue::Integer(42)})}})})}}}, + {"d", + {{"array", FieldValue::Array({FieldValue::Integer(42)})}, + {"array2", FieldValue::Array({FieldValue::String("bingo")})}}}}); + // Search for 42 + const AggregateQuery aggregate_query = + collection.WhereArrayContains("array", FieldValue::Integer(42)).Count(); + const AggregateQuerySnapshot aggregate_snapshot = + ReadAggregate(aggregate_query); + EXPECT_EQ(3, aggregate_snapshot.count()); + EXPECT_EQ(aggregate_query, aggregate_snapshot.query()); + + // NOTE: The backend doesn't currently support null, NaN, objects, or arrays, + // so there isn't much of anything else interesting to test. +} + +TEST_F(AggregateCountTest, TestQueriesCanUseInFilters) { + CollectionReference collection = Collection( + {{"a", {{"zip", FieldValue::Integer(98101)}}}, + {"b", {{"zip", FieldValue::Integer(98102)}}}, + {"c", {{"zip", FieldValue::Integer(98103)}}}, + {"d", {{"zip", FieldValue::Array({FieldValue::Integer(98101)})}}}, + {"e", + {{"zip", + FieldValue::Array( + {FieldValue::String("98101"), + FieldValue::Map({{"zip", FieldValue::Integer(98101)}})})}}}, + {"f", {{"zip", FieldValue::Map({{"code", FieldValue::Integer(500)}})}}}, + {"g", + {{"zip", FieldValue::Array({FieldValue::Integer(98101), + FieldValue::Integer(98102)})}}}}); + // Search for zips matching 98101, 98103, or [98101, 98102]. + const AggregateQuery aggregate_query1 = + collection + .WhereIn("zip", + {FieldValue::Integer(98101), FieldValue::Integer(98103), + FieldValue::Array({FieldValue::Integer(98101), + FieldValue::Integer(98102)})}) + .Count(); + const AggregateQuerySnapshot aggregate_snapshot1 = + ReadAggregate(aggregate_query1); + EXPECT_EQ(3, aggregate_snapshot1.count()); + EXPECT_EQ(aggregate_query1, aggregate_snapshot1.query()); + + // With objects. + const AggregateQuery aggregate_query2 = + collection + .WhereIn("zip", + {FieldValue::Map({{"code", FieldValue::Integer(500)}})}) + .Count(); + const AggregateQuerySnapshot aggregate_snapshot2 = + ReadAggregate(aggregate_query2); + EXPECT_EQ(1, aggregate_snapshot2.count()); + EXPECT_EQ(aggregate_query2, aggregate_snapshot2.query()); +} + +TEST_F(AggregateCountTest, TestQueriesCanUseInFiltersWithDocIds) { + CollectionReference collection = + Collection({{"aa", {{"key", FieldValue::String("aa")}}}, + {"ab", {{"key", FieldValue::String("ab")}}}, + {"ba", {{"key", FieldValue::String("ba")}}}, + {"bb", {{"key", FieldValue::String("bb")}}}}); + + const AggregateQuery aggregate_query = + collection + .WhereIn(FieldPath::DocumentId(), + {FieldValue::String("aa"), FieldValue::String("ab")}) + .Count(); + const AggregateQuerySnapshot aggregate_snapshot = + ReadAggregate(aggregate_query); + EXPECT_EQ(2, aggregate_snapshot.count()); + EXPECT_EQ(aggregate_query, aggregate_snapshot.query()); +} + +TEST_F(AggregateCountTest, TestQueriesCanUseNotInFilters) { + // These documents are ordered by value in "zip" since the NotEqual filter is + // an inequality, which results in documents being sorted by value. + std::map docs = { + {"a", {{"zip", FieldValue::Double(NAN)}}}, + {"b", {{"zip", FieldValue::Integer(91102)}}}, + {"c", {{"zip", FieldValue::Integer(98101)}}}, + {"d", {{"zip", FieldValue::Integer(98103)}}}, + {"e", {{"zip", FieldValue::Array({FieldValue::Integer(98101)})}}}, + {"f", + {{"zip", FieldValue::Array({FieldValue::Integer(98101), + FieldValue::Integer(98102)})}}}, + {"g", + {{"zip", FieldValue::Array({FieldValue::String("98101"), + FieldValue::Map({{"zip", FieldValue::Integer( + 98101)}})})}}}, + {"h", {{"zip", FieldValue::Map({{"code", FieldValue::Integer(500)}})}}}, + {"i", {{"code", FieldValue::Integer(500)}}}, + {"j", MapFieldValue{{"zip", FieldValue::Null()}}}, + }; + CollectionReference collection = Collection(docs); + + // Search for zips not matching 98101, 98103 or [98101, 98102]. + const AggregateQuery aggregate_query = + collection + .WhereNotIn("zip", + {{FieldValue::Integer(98101), FieldValue::Integer(98103), + FieldValue::Array({{FieldValue::Integer(98101), + FieldValue::Integer(98102)}})}}) + .Count(); + const AggregateQuerySnapshot aggregate_snapshot = + ReadAggregate(aggregate_query); + EXPECT_EQ(5, aggregate_snapshot.count()); + EXPECT_EQ(aggregate_query, aggregate_snapshot.query()); +} + +TEST_F(AggregateCountTest, TestQueriesCanUseNotInFiltersWithObject) { + // These documents are ordered by value in "zip" since the NotEqual filter is + // an inequality, which results in documents being sorted by value. + std::map docs = { + {"a", {{"zip", FieldValue::Double(NAN)}}}, + {"b", {{"zip", FieldValue::Integer(91102)}}}, + {"c", {{"zip", FieldValue::Integer(98101)}}}, + {"d", {{"zip", FieldValue::Integer(98103)}}}, + {"e", {{"zip", FieldValue::Array({FieldValue::Integer(98101)})}}}, + {"f", + {{"zip", FieldValue::Array({FieldValue::Integer(98101), + FieldValue::Integer(98102)})}}}, + {"g", + {{"zip", FieldValue::Array({FieldValue::String("98101"), + FieldValue::Map({{"zip", FieldValue::Integer( + 98101)}})})}}}, + {"h", {{"zip", FieldValue::Map({{"code", FieldValue::Integer(500)}})}}}, + {"i", {{"code", FieldValue::Integer(500)}}}, + {"j", MapFieldValue{{"zip", FieldValue::Null()}}}, + }; + CollectionReference collection = Collection(docs); + + const AggregateQuery aggregate_query = + collection + .WhereNotIn("zip", + {{FieldValue::Map({{"code", FieldValue::Integer(500)}})}}) + .Count(); + const AggregateQuerySnapshot aggregate_snapshot = + ReadAggregate(aggregate_query); + EXPECT_EQ(7, aggregate_snapshot.count()); + EXPECT_EQ(aggregate_query, aggregate_snapshot.query()); +} + +TEST_F(AggregateCountTest, TestQueriesCanUseNotInFiltersWithNull) { + // These documents are ordered by value in "zip" since the NotEqual filter is + // an inequality, which results in documents being sorted by value. + std::map docs = { + {"a", {{"zip", FieldValue::Double(NAN)}}}, + {"b", {{"zip", FieldValue::Integer(91102)}}}, + {"c", {{"zip", FieldValue::Integer(98101)}}}, + {"d", {{"zip", FieldValue::Integer(98103)}}}, + {"e", {{"zip", FieldValue::Array({FieldValue::Integer(98101)})}}}, + {"f", + {{"zip", FieldValue::Array({FieldValue::Integer(98101), + FieldValue::Integer(98102)})}}}, + {"g", + {{"zip", FieldValue::Array({FieldValue::String("98101"), + FieldValue::Map({{"zip", FieldValue::Integer( + 98101)}})})}}}, + {"h", {{"zip", FieldValue::Map({{"code", FieldValue::Integer(500)}})}}}, + {"i", {{"code", FieldValue::Integer(500)}}}, + {"j", MapFieldValue{{"zip", FieldValue::Null()}}}, + }; + CollectionReference collection = Collection(docs); + + // With Null, this leads to no result. + const AggregateQuery aggregate_query = + collection.WhereNotIn("zip", {{FieldValue::Null()}}).Count(); + const AggregateQuerySnapshot aggregate_snapshot = + ReadAggregate(aggregate_query); + EXPECT_EQ(0, aggregate_snapshot.count()); + EXPECT_EQ(aggregate_query, aggregate_snapshot.query()); +} + +TEST_F(AggregateCountTest, TestQueriesCanUseNotInFiltersWithNan) { + // These documents are ordered by value in "zip" since the NotEqual filter is + // an inequality, which results in documents being sorted by value. + std::map docs = { + {"a", {{"zip", FieldValue::Double(NAN)}}}, + {"b", {{"zip", FieldValue::Integer(91102)}}}, + {"c", {{"zip", FieldValue::Integer(98101)}}}, + {"d", {{"zip", FieldValue::Integer(98103)}}}, + {"e", {{"zip", FieldValue::Array({FieldValue::Integer(98101)})}}}, + {"f", + {{"zip", FieldValue::Array({FieldValue::Integer(98101), + FieldValue::Integer(98102)})}}}, + {"g", + {{"zip", FieldValue::Array({FieldValue::String("98101"), + FieldValue::Map({{"zip", FieldValue::Integer( + 98101)}})})}}}, + {"h", {{"zip", FieldValue::Map({{"code", FieldValue::Integer(500)}})}}}, + {"i", {{"code", FieldValue::Integer(500)}}}, + {"j", MapFieldValue{{"zip", FieldValue::Null()}}}, + }; + CollectionReference collection = Collection(docs); + + // With NAN. + const AggregateQuery aggregate_query = + collection.WhereNotIn("zip", {{FieldValue::Double(NAN)}}).Count(); + const AggregateQuerySnapshot aggregate_snapshot = + ReadAggregate(aggregate_query); + // TODO(b/272502845): NaN Handling + // EXPECT_EQ(7, aggregate_snapshot.count()); + EXPECT_EQ(8, aggregate_snapshot.count()); + EXPECT_EQ(aggregate_query, aggregate_snapshot.query()); +} + +TEST_F(AggregateCountTest, TestQueriesCanUseNotInFiltersWithNanAndNumber) { + // These documents are ordered by value in "zip" since the NotEqual filter is + // an inequality, which results in documents being sorted by value. + std::map docs = { + {"a", {{"zip", FieldValue::Double(NAN)}}}, + {"b", {{"zip", FieldValue::Integer(91102)}}}, + {"c", {{"zip", FieldValue::Integer(98101)}}}, + {"d", {{"zip", FieldValue::Integer(98103)}}}, + {"e", {{"zip", FieldValue::Array({FieldValue::Integer(98101)})}}}, + {"f", + {{"zip", FieldValue::Array({FieldValue::Integer(98101), + FieldValue::Integer(98102)})}}}, + {"g", + {{"zip", FieldValue::Array({FieldValue::String("98101"), + FieldValue::Map({{"zip", FieldValue::Integer( + 98101)}})})}}}, + {"h", {{"zip", FieldValue::Map({{"code", FieldValue::Integer(500)}})}}}, + {"i", {{"code", FieldValue::Integer(500)}}}, + {"j", MapFieldValue{{"zip", FieldValue::Null()}}}, + }; + CollectionReference collection = Collection(docs); + + const AggregateQuery aggregate_query = + collection + .WhereNotIn("zip", + {{FieldValue::Double(NAN), FieldValue::Integer(98101)}}) + .Count(); + const AggregateQuerySnapshot aggregate_snapshot = + ReadAggregate(aggregate_query); + // TODO(b/272502845): NaN Handling + // EXPECT_EQ(6, aggregate_snapshot.count()); + EXPECT_EQ(7, aggregate_snapshot.count()); + EXPECT_EQ(aggregate_query, aggregate_snapshot.query()); +} + +TEST_F(AggregateCountTest, TestQueriesCanUseNotInFiltersWithDocIds) { + MapFieldValue doc_a = {{"key", FieldValue::String("aa")}}; + MapFieldValue doc_b = {{"key", FieldValue::String("ab")}}; + MapFieldValue doc_c = {{"key", FieldValue::String("ba")}}; + MapFieldValue doc_d = {{"key", FieldValue::String("bb")}}; + + CollectionReference collection = + Collection({{"aa", doc_a}, {"ab", doc_b}, {"ba", doc_c}, {"bb", doc_d}}); + + const AggregateQuery aggregate_query = + collection + .WhereNotIn(FieldPath::DocumentId(), + {{FieldValue::String("aa"), FieldValue::String("ab")}}) + .Count(); + const AggregateQuerySnapshot aggregate_snapshot = + ReadAggregate(aggregate_query); + EXPECT_EQ(2, aggregate_snapshot.count()); + EXPECT_EQ(aggregate_query, aggregate_snapshot.query()); +} + +TEST_F(AggregateCountTest, TestQueriesCanUseArrayContainsAnyFilters) { + CollectionReference collection = Collection( + {{"a", {{"array", FieldValue::Array({FieldValue::Integer(42)})}}}, + {"b", + {{"array", + FieldValue::Array({FieldValue::String("a"), FieldValue::Integer(42), + FieldValue::String("c")})}}}, + {"c", + {{"array", + FieldValue::Array( + {FieldValue::Double(41.999), FieldValue::String("42"), + FieldValue::Map( + {{"a", FieldValue::Array({FieldValue::Integer(42)})}})})}}}, + {"d", + {{"array", FieldValue::Array({FieldValue::Integer(42)})}, + {"array2", FieldValue::Array({FieldValue::String("bingo")})}}}, + {"e", {{"array", FieldValue::Array({FieldValue::Integer(43)})}}}, + {"f", + {{"array", FieldValue::Array( + {FieldValue::Map({{"a", FieldValue::Integer(42)}})})}}}, + {"g", {{"array", FieldValue::Integer(42)}}}}); + + // Search for "array" to contain [42, 43] + const AggregateQuery aggregate_query1 = + collection + .WhereArrayContainsAny( + "array", {FieldValue::Integer(42), FieldValue::Integer(43)}) + .Count(); + const AggregateQuerySnapshot aggregate_snapshot1 = + ReadAggregate(aggregate_query1); + EXPECT_EQ(4, aggregate_snapshot1.count()); + EXPECT_EQ(aggregate_query1, aggregate_snapshot1.query()); + + // With objects + const AggregateQuery aggregate_query2 = + collection + .WhereArrayContainsAny( + "array", {FieldValue::Map({{"a", FieldValue::Integer(42)}})}) + .Count(); + const AggregateQuerySnapshot aggregate_snapshot2 = + ReadAggregate(aggregate_query2); + EXPECT_EQ(1, aggregate_snapshot2.count()); + EXPECT_EQ(aggregate_query2, aggregate_snapshot2.query()); +} + +TEST_F(AggregateCountTest, TestCollectionGroupQueries) { + Firestore* db = TestFirestore(); + // Use .Document() to get a random collection group name to use but ensure it + // starts with 'b' for predictable ordering. + std::string collection_group = "b" + db->Collection("foo").Document().id(); + + std::string doc_paths[] = { + "abc/123/" + collection_group + "/cg-doc1", + "abc/123/" + collection_group + "/cg-doc2", + collection_group + "/cg-doc3", + collection_group + "/cg-doc4", + "def/456/" + collection_group + "/cg-doc5", + collection_group + "/virtual-doc/nested-coll/not-cg-doc", + "x" + collection_group + "/not-cg-doc", + collection_group + "x/not-cg-doc", + "abc/123/" + collection_group + "x/not-cg-doc", + "abc/123/x" + collection_group + "/not-cg-doc", + "abc/" + collection_group, + }; + + WriteBatch batch = db->batch(); + for (const auto& doc_path : doc_paths) { + batch.Set(db->Document(doc_path), {{"x", FieldValue::Integer(1)}}); + } + Await(batch.Commit()); + + const AggregateQuery aggregate_query = + db->CollectionGroup(collection_group).Count(); + const AggregateQuerySnapshot aggregate_snapshot = + ReadAggregate(aggregate_query); + EXPECT_EQ(5, aggregate_snapshot.count()); + EXPECT_EQ(aggregate_query, aggregate_snapshot.query()); +} + +TEST_F(AggregateCountTest, + TestCollectionGroupQueriesWithStartAtEndAtWithArbitraryDocumentIds) { + Firestore* db = TestFirestore(); + // Use .Document() to get a random collection group name to use but ensure it + // starts with 'b' for predictable ordering. + std::string collection_group = "b" + db->Collection("foo").Document().id(); + + std::string doc_paths[] = { + "a/a/" + collection_group + "/cg-doc1", + "a/b/a/b/" + collection_group + "/cg-doc2", + "a/b/" + collection_group + "/cg-doc3", + "a/b/c/d/" + collection_group + "/cg-doc4", + "a/c/" + collection_group + "/cg-doc5", + collection_group + "/cg-doc6", + "a/b/nope/nope", + }; + + WriteBatch batch = db->batch(); + for (const auto& doc_path : doc_paths) { + batch.Set(db->Document(doc_path), {{"x", FieldValue::Integer(1)}}); + } + Await(batch.Commit()); + + const AggregateQuery aggregate_query = + db->CollectionGroup(collection_group) + .OrderBy(FieldPath::DocumentId()) + .StartAt({FieldValue::String("a/b")}) + .EndAt({FieldValue::String("a/b0")}) + .Count(); + const AggregateQuerySnapshot aggregate_snapshot = + ReadAggregate(aggregate_query); + EXPECT_EQ(3, aggregate_snapshot.count()); + EXPECT_EQ(aggregate_query, aggregate_snapshot.query()); +} + +TEST_F(AggregateCountTest, + TestCollectionGroupQueriesWithWhereFiltersOnArbitraryDocumentIds) { + Firestore* db = TestFirestore(); + // Use .Document() to get a random collection group name to use but ensure it + // starts with 'b' for predictable ordering. + std::string collection_group = "b" + db->Collection("foo").Document().id(); + + std::string doc_paths[] = { + "a/a/" + collection_group + "/cg-doc1", + "a/b/a/b/" + collection_group + "/cg-doc2", + "a/b/" + collection_group + "/cg-doc3", + "a/b/c/d/" + collection_group + "/cg-doc4", + "a/c/" + collection_group + "/cg-doc5", + collection_group + "/cg-doc6", + "a/b/nope/nope", + }; + + WriteBatch batch = db->batch(); + for (const auto& doc_path : doc_paths) { + batch.Set(db->Document(doc_path), {{"x", FieldValue::Integer(1)}}); + } + Await(batch.Commit()); + + const AggregateQuery aggregate_query1 = + db->CollectionGroup(collection_group) + .WhereGreaterThanOrEqualTo(FieldPath::DocumentId(), + FieldValue::String("a/b")) + .WhereLessThanOrEqualTo(FieldPath::DocumentId(), + FieldValue::String("a/b0")) + .Count(); + const AggregateQuerySnapshot aggregate_snapshot1 = + ReadAggregate(aggregate_query1); + EXPECT_EQ(3, aggregate_snapshot1.count()); + EXPECT_EQ(aggregate_query1, aggregate_snapshot1.query()); + + const AggregateQuery aggregate_query2 = + db->CollectionGroup(collection_group) + .WhereGreaterThan(FieldPath::DocumentId(), FieldValue::String("a/b")) + .WhereLessThan( + FieldPath::DocumentId(), + FieldValue::String("a/b/" + collection_group + "/cg-doc3")) + .Count(); + const AggregateQuerySnapshot aggregate_snapshot2 = + ReadAggregate(aggregate_query2); + EXPECT_EQ(1, aggregate_snapshot2.count()); + EXPECT_EQ(aggregate_query2, aggregate_snapshot2.query()); +} + +#if defined(__ANDROID__) +TEST(QueryTestAndroidStub, Construction) { + testutil::AssertWrapperConstructionContract(); +} + +TEST(QueryTestAndroidStub, Assignment) { + testutil::AssertWrapperAssignmentContract(); +} +#endif // defined(__ANDROID__) + +} // namespace firestore +} // namespace firebase diff --git a/firestore/integration_test_internal/src/aggregate_query_snapshot_test.cc b/firestore/integration_test_internal/src/aggregate_query_snapshot_test.cc new file mode 100644 index 0000000000..2e73d9cc49 --- /dev/null +++ b/firestore/integration_test_internal/src/aggregate_query_snapshot_test.cc @@ -0,0 +1,572 @@ +/* + * Copyright 2023 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. + */ + +#include "firestore_integration_test.h" + +#include "firebase/firestore.h" + +#include "gtest/gtest.h" + +#if defined(__ANDROID__) +#include "firestore/src/android/aggregate_query_android.h" +#include "firestore/src/android/aggregate_query_snapshot_android.h" +#include "firestore/src/android/converter_android.h" +#include "firestore/src/jni/object.h" +#else +#include "firestore/src/main/aggregate_query_snapshot_main.h" +#include "firestore/src/main/converter_main.h" +#endif // defined(__ANDROID__) + +namespace firebase { +namespace firestore { + +class AggregateQuerySnapshotTest : public FirestoreIntegrationTest { + protected: + static AggregateQuerySnapshot TestAggregateQuerySnapshot( + AggregateQuery aggregate_query, const int count); +}; + +#if defined(__ANDROID__) +AggregateQuerySnapshot AggregateQuerySnapshotTest::TestAggregateQuerySnapshot( + firebase::firestore::AggregateQuery aggregate_query, const int count) { + AggregateQueryInternal* internal = GetInternal(&aggregate_query); + FirestoreInternal* firestoreInternal = internal->firestore_internal(); + jni::Env env = firestoreInternal->GetEnv(); + return AggregateQuerySnapshotInternal::Create(env, *internal, count); +} +#else +AggregateQuerySnapshot AggregateQuerySnapshotTest::TestAggregateQuerySnapshot( + firebase::firestore::AggregateQuery aggregate_query, const int count) { + api::AggregateQuery aggregateQuery = + GetInternal(&aggregate_query)->aggregate_query_; + return MakePublic( + AggregateQuerySnapshotInternal(std::move(aggregateQuery), count)); +} +#endif // defined(__ANDROID__) + +std::size_t AggregateQuerySnapshotHash(const AggregateQuerySnapshot& snapshot) { + return snapshot.Hash(); +} + +namespace { + +TEST_F(AggregateQuerySnapshotTest, DefaultConstructorReturnsInvalidObject) { + AggregateQuerySnapshot snapshot; + EXPECT_EQ(snapshot.query(), AggregateQuery()); + EXPECT_EQ(snapshot.count(), 0); + EXPECT_FALSE(snapshot.is_valid()); +} + +TEST_F(AggregateQuerySnapshotTest, + CopyConstructorAppliedToDefaultObjectReturnsEqualObject) { + AggregateQuerySnapshot snapshot; + + EXPECT_EQ(snapshot.count(), 0); + EXPECT_EQ(snapshot.query(), AggregateQuery()); + EXPECT_FALSE(snapshot.is_valid()); + + AggregateQuerySnapshot copied_snapshot(snapshot); + + EXPECT_EQ(snapshot.count(), 0); + EXPECT_EQ(snapshot.query(), AggregateQuery()); + EXPECT_FALSE(snapshot.is_valid()); + + EXPECT_EQ(copied_snapshot.count(), 0); + EXPECT_EQ(copied_snapshot.query(), AggregateQuery()); + EXPECT_FALSE(copied_snapshot.is_valid()); +} + +TEST_F(AggregateQuerySnapshotTest, + CopyConstructorAppliedToValidObjectReturnsEqualObject) { + const AggregateQuery aggregate_query = + TestFirestore()->Collection("foo").Limit(10).Count(); + + AggregateQuerySnapshot snapshot = + TestAggregateQuerySnapshot(aggregate_query, 5); + + EXPECT_EQ(snapshot.count(), 5); + EXPECT_EQ(snapshot.query(), aggregate_query); + EXPECT_TRUE(snapshot.is_valid()); + + AggregateQuerySnapshot copied_snapshot(snapshot); + + EXPECT_EQ(snapshot.count(), 5); + EXPECT_EQ(snapshot.query(), aggregate_query); + EXPECT_TRUE(snapshot.is_valid()); + + EXPECT_EQ(copied_snapshot.count(), 5); + EXPECT_EQ(copied_snapshot.query(), aggregate_query); + EXPECT_TRUE(copied_snapshot.is_valid()); +} + +TEST_F( + AggregateQuerySnapshotTest, + DefaultObjectCopyAssignmentOperatorAppliedToValidObjectReturnsEqualObject) { + const AggregateQuery aggregate_query = + TestFirestore()->Collection("foo").Limit(10).Count(); + const AggregateQuerySnapshot snapshot = + TestAggregateQuerySnapshot(aggregate_query, 7); + + AggregateQuerySnapshot snapshot_copy_dest; + + EXPECT_EQ(snapshot_copy_dest.count(), 0); + EXPECT_EQ(snapshot_copy_dest.query(), AggregateQuery()); + EXPECT_FALSE(snapshot_copy_dest.is_valid()); + + snapshot_copy_dest = snapshot; + + EXPECT_EQ(snapshot.count(), 7); + EXPECT_EQ(snapshot.query(), aggregate_query); + EXPECT_TRUE(snapshot.is_valid()); + + EXPECT_EQ(snapshot_copy_dest.count(), 7); + EXPECT_EQ(snapshot_copy_dest.query(), aggregate_query); + EXPECT_TRUE(snapshot_copy_dest.is_valid()); +} + +TEST_F( + AggregateQuerySnapshotTest, + DefaultObjectCopyAssignmentOperatorAppliedToDefaultObjectReturnsEqualObject) { + const AggregateQuerySnapshot snapshot; + + EXPECT_EQ(snapshot.count(), 0); + EXPECT_EQ(snapshot.query(), AggregateQuery()); + EXPECT_FALSE(snapshot.is_valid()); + + AggregateQuerySnapshot snapshot_copy_dest; + + EXPECT_EQ(snapshot_copy_dest.count(), 0); + EXPECT_EQ(snapshot_copy_dest.query(), AggregateQuery()); + EXPECT_FALSE(snapshot_copy_dest.is_valid()); + + snapshot_copy_dest = snapshot; + + EXPECT_EQ(snapshot_copy_dest.count(), 0); + EXPECT_EQ(snapshot_copy_dest.query(), AggregateQuery()); + EXPECT_FALSE(snapshot_copy_dest.is_valid()); + + EXPECT_EQ(snapshot.count(), 0); + EXPECT_EQ(snapshot.query(), AggregateQuery()); + EXPECT_FALSE(snapshot.is_valid()); +} + +TEST_F( + AggregateQuerySnapshotTest, + ValidObjectCopyAssignmentOperatorAppliedToValidObjectReturnsEqualObject) { + const AggregateQuerySnapshot snapshot; + + EXPECT_EQ(snapshot.count(), 0); + EXPECT_EQ(snapshot.query(), AggregateQuery()); + EXPECT_FALSE(snapshot.is_valid()); + + const AggregateQuery aggregate_query = + TestFirestore()->Collection("foo").Limit(10).Count(); + + AggregateQuerySnapshot snapshot_copy_dest = + TestAggregateQuerySnapshot(aggregate_query, 7); + + EXPECT_EQ(snapshot_copy_dest.count(), 7); + EXPECT_EQ(snapshot_copy_dest.query(), aggregate_query); + EXPECT_TRUE(snapshot_copy_dest.is_valid()); + + snapshot_copy_dest = snapshot; + + EXPECT_EQ(snapshot.count(), 0); + EXPECT_EQ(snapshot.query(), AggregateQuery()); + EXPECT_FALSE(snapshot.is_valid()); + + EXPECT_EQ(snapshot_copy_dest.count(), 0); + EXPECT_EQ(snapshot_copy_dest.query(), AggregateQuery()); + EXPECT_FALSE(snapshot_copy_dest.is_valid()); +} + +TEST_F( + AggregateQuerySnapshotTest, + ValidObjectCopyAssignmentOperatorAppliedToDefaultObjectReturnsEqualObject) { + const AggregateQuery aggregate_query1 = + TestFirestore()->Collection("foo").Limit(10).Count(); + const AggregateQuery aggregate_query2 = + TestFirestore()->Collection("bar").Limit(20).Count(); + + const AggregateQuerySnapshot snapshot = + TestAggregateQuerySnapshot(aggregate_query1, 1); + + EXPECT_EQ(snapshot.count(), 1); + EXPECT_EQ(snapshot.query(), aggregate_query1); + EXPECT_TRUE(snapshot.is_valid()); + + AggregateQuerySnapshot snapshot_copy_dest = + TestAggregateQuerySnapshot(aggregate_query2, 2); + + EXPECT_EQ(snapshot_copy_dest.count(), 2); + EXPECT_EQ(snapshot_copy_dest.query(), aggregate_query2); + EXPECT_TRUE(snapshot_copy_dest.is_valid()); + + snapshot_copy_dest = snapshot; + + EXPECT_EQ(snapshot.count(), 1); + EXPECT_EQ(snapshot.query(), aggregate_query1); + EXPECT_TRUE(snapshot.is_valid()); + + EXPECT_EQ(snapshot_copy_dest.count(), 1); + EXPECT_EQ(snapshot_copy_dest.query(), aggregate_query1); + EXPECT_TRUE(snapshot_copy_dest.is_valid()); +} + +TEST_F(AggregateQuerySnapshotTest, + CopyAssignmentAppliedSelfReturnsEqualObject) { + const AggregateQuery aggregate_query = + TestFirestore()->Collection("foo").Limit(10).Count(); + + AggregateQuerySnapshot snapshot = + TestAggregateQuerySnapshot(aggregate_query, 7); + + EXPECT_EQ(snapshot.count(), 7); + EXPECT_EQ(snapshot.query(), aggregate_query); + EXPECT_TRUE(snapshot.is_valid()); + + snapshot = snapshot; + + EXPECT_EQ(snapshot.count(), 7); + EXPECT_EQ(snapshot.query(), aggregate_query); + EXPECT_TRUE(snapshot.is_valid()); +} + +TEST_F(AggregateQuerySnapshotTest, + MoveConstructorAppliedToValidObjectReturnsEqualObject) { + const AggregateQuery aggregate_query = + TestFirestore()->Collection("foo").Limit(10).Count(); + + AggregateQuerySnapshot snapshot = + TestAggregateQuerySnapshot(aggregate_query, 11); + + EXPECT_EQ(snapshot.count(), 11); + EXPECT_EQ(snapshot.query(), aggregate_query); + EXPECT_TRUE(snapshot.is_valid()); + + AggregateQuerySnapshot moved_snapshot_dest(std::move(snapshot)); + + EXPECT_EQ(snapshot.count(), 0); + EXPECT_EQ(snapshot.query(), AggregateQuery()); + EXPECT_FALSE(snapshot.is_valid()); + + EXPECT_EQ(moved_snapshot_dest.count(), 11); + EXPECT_EQ(moved_snapshot_dest.query(), aggregate_query); + EXPECT_TRUE(moved_snapshot_dest.is_valid()); +} + +TEST_F(AggregateQuerySnapshotTest, + MoveConstructorAppliedToDefaultObjectReturnsEqualObject) { + AggregateQuerySnapshot snapshot; + + EXPECT_EQ(snapshot.count(), 0); + EXPECT_EQ(snapshot.query(), AggregateQuery()); + EXPECT_FALSE(snapshot.is_valid()); + + AggregateQuerySnapshot moved_snapshot_dest(std::move(snapshot)); + + EXPECT_EQ(snapshot.count(), 0); + EXPECT_EQ(snapshot.query(), AggregateQuery()); + EXPECT_FALSE(snapshot.is_valid()); + + EXPECT_EQ(snapshot.count(), 0); + EXPECT_EQ(snapshot.query(), AggregateQuery()); + EXPECT_FALSE(snapshot.is_valid()); +} + +TEST_F( + AggregateQuerySnapshotTest, + DefaultObjectMoveAssignmentOperatorAppliedToValidObjectReturnsEqualObject) { + const AggregateQuery aggregate_query = + TestFirestore()->Collection("foo").Limit(10).Count(); + + AggregateQuerySnapshot snapshot = + TestAggregateQuerySnapshot(aggregate_query, 3); + + EXPECT_EQ(snapshot.count(), 3); + EXPECT_EQ(snapshot.query(), aggregate_query); + EXPECT_TRUE(snapshot.is_valid()); + + AggregateQuerySnapshot snapshot_move_dest = std::move(snapshot); + + EXPECT_EQ(snapshot.count(), 0); + EXPECT_EQ(snapshot.query(), AggregateQuery()); + EXPECT_FALSE(snapshot.is_valid()); + + EXPECT_EQ(snapshot_move_dest.count(), 3); + EXPECT_EQ(snapshot_move_dest.query(), aggregate_query); + EXPECT_TRUE(snapshot_move_dest.is_valid()); +} + +TEST_F( + AggregateQuerySnapshotTest, + DefaultObjectMoveAssignmentOperatorAppliedToDefaultObjectReturnsEqualObject) { + AggregateQuerySnapshot snapshot; + + EXPECT_EQ(snapshot.count(), 0); + EXPECT_EQ(snapshot.query(), AggregateQuery()); + EXPECT_FALSE(snapshot.is_valid()); + + AggregateQuerySnapshot snapshot_move_dest = std::move(snapshot); + + EXPECT_EQ(snapshot.count(), 0); + EXPECT_EQ(snapshot.query(), AggregateQuery()); + EXPECT_FALSE(snapshot.is_valid()); + + EXPECT_EQ(snapshot_move_dest.count(), 0); + EXPECT_EQ(snapshot_move_dest.query(), AggregateQuery()); + EXPECT_FALSE(snapshot_move_dest.is_valid()); +} + +TEST_F( + AggregateQuerySnapshotTest, + ValidObjectMoveAssignmentOperatorAppliedToValidObjectReturnsEqualObject) { + const AggregateQuery aggregate_query1 = + TestFirestore()->Collection("foo").Limit(10).Count(); + const AggregateQuery aggregate_query2 = + TestFirestore()->Collection("bar").Limit(20).Count(); + + AggregateQuerySnapshot snapshot = + TestAggregateQuerySnapshot(aggregate_query1, 3); + + EXPECT_EQ(snapshot.count(), 3); + EXPECT_EQ(snapshot.query(), aggregate_query1); + EXPECT_TRUE(snapshot.is_valid()); + + AggregateQuerySnapshot snapshot_move_dest = + TestAggregateQuerySnapshot(aggregate_query2, 6); + + EXPECT_EQ(snapshot_move_dest.count(), 6); + EXPECT_EQ(snapshot_move_dest.query(), aggregate_query2); + EXPECT_TRUE(snapshot_move_dest.is_valid()); + + snapshot_move_dest = std::move(snapshot); + + EXPECT_EQ(snapshot.count(), 0); + EXPECT_EQ(snapshot.query(), AggregateQuery()); + EXPECT_FALSE(snapshot.is_valid()); + + EXPECT_EQ(snapshot_move_dest.count(), 3); + EXPECT_EQ(snapshot_move_dest.query(), aggregate_query1); + EXPECT_TRUE(snapshot_move_dest.is_valid()); +} + +TEST_F( + AggregateQuerySnapshotTest, + ValidObjectMoveAssignmentOperatorAppliedToDefaultObjectReturnsEqualObject) { + AggregateQuerySnapshot snapshot; + + EXPECT_EQ(snapshot.count(), 0); + EXPECT_EQ(snapshot.query(), AggregateQuery()); + EXPECT_FALSE(snapshot.is_valid()); + + const AggregateQuery aggregate_query = + TestFirestore()->Collection("foo").Limit(10).Count(); + + AggregateQuerySnapshot snapshot_move_dest = + TestAggregateQuerySnapshot(aggregate_query, 99); + + EXPECT_EQ(snapshot_move_dest.count(), 99); + EXPECT_EQ(snapshot_move_dest.query(), aggregate_query); + EXPECT_TRUE(snapshot_move_dest.is_valid()); + + snapshot_move_dest = std::move(snapshot); + + EXPECT_EQ(snapshot.count(), 0); + EXPECT_EQ(snapshot.query(), AggregateQuery()); + EXPECT_FALSE(snapshot.is_valid()); + + EXPECT_EQ(snapshot_move_dest.count(), 0); + EXPECT_EQ(snapshot_move_dest.query(), AggregateQuery()); + EXPECT_FALSE(snapshot_move_dest.is_valid()); +} + +TEST_F(AggregateQuerySnapshotTest, + MoveAssignmentOperatorAppliedToSelfReturnsEqualObject) { + const AggregateQuery aggregate_query = + TestFirestore()->Collection("foo").Limit(10).Count(); + + AggregateQuerySnapshot snapshot = + TestAggregateQuerySnapshot(aggregate_query, 99); + + EXPECT_EQ(snapshot.count(), 99); + EXPECT_EQ(snapshot.query(), aggregate_query); + EXPECT_TRUE(snapshot.is_valid()); + + snapshot = std::move(snapshot); + + EXPECT_EQ(snapshot.count(), 99); + EXPECT_EQ(snapshot.query(), aggregate_query); + EXPECT_TRUE(snapshot.is_valid()); +} + +TEST_F(AggregateQuerySnapshotTest, + IdenticalSnapshotFromCollectionQueriesWithLimitShouldBeEqual) { + CollectionReference collection = + Collection({{"a", {{"k", FieldValue::String("a")}}}, + {"b", {{"k", FieldValue::String("b")}}}, + {"c", {{"k", FieldValue::String("c")}}}}); + AggregateQuerySnapshot snapshot1 = ReadAggregate(collection.Limit(1).Count()); + AggregateQuerySnapshot snapshot2 = ReadAggregate(collection.Limit(1).Count()); + + EXPECT_TRUE(snapshot1 == snapshot1); + EXPECT_TRUE(snapshot1 == snapshot2); + + EXPECT_FALSE(snapshot1 != snapshot1); + EXPECT_FALSE(snapshot1 != snapshot2); +} + +TEST_F(AggregateQuerySnapshotTest, + IdenticalSnapshotFromCollectionQueriesShouldBeEqual) { + CollectionReference collection = + Collection({{"a", {{"k", FieldValue::String("a")}}}, + {"b", {{"k", FieldValue::String("b")}}}, + {"c", {{"k", FieldValue::String("c")}}}}); + AggregateQuerySnapshot snapshot1 = ReadAggregate(collection.Count()); + AggregateQuerySnapshot snapshot2 = ReadAggregate(collection.Count()); + + EXPECT_TRUE(snapshot1 == snapshot1); + EXPECT_TRUE(snapshot1 == snapshot2); + + EXPECT_FALSE(snapshot1 != snapshot1); + EXPECT_FALSE(snapshot1 != snapshot2); +} + +TEST_F(AggregateQuerySnapshotTest, + IdenticalDefaultAggregateSnapshotShouldBeEqual) { + AggregateQuerySnapshot snapshot1; + AggregateQuerySnapshot snapshot2; + + EXPECT_TRUE(snapshot1 == snapshot1); + EXPECT_TRUE(snapshot1 == snapshot2); + + EXPECT_FALSE(snapshot1 != snapshot1); + EXPECT_FALSE(snapshot1 != snapshot2); +} + +TEST_F(AggregateQuerySnapshotTest, NonEquality) { + CollectionReference collection = + Collection({{"a", {{"k", FieldValue::String("a")}}}, + {"b", {{"k", FieldValue::String("b")}}}, + {"c", {{"k", FieldValue::String("c")}}}}); + AggregateQuerySnapshot snapshot1 = ReadAggregate( + collection.WhereEqualTo("k", FieldValue::String("d")).Count()); + AggregateQuerySnapshot snapshot2 = ReadAggregate(collection.Limit(1).Count()); + AggregateQuerySnapshot snapshot3 = ReadAggregate(collection.Limit(3).Count()); + AggregateQuerySnapshot snapshot4 = ReadAggregate(collection.Count()); + AggregateQuerySnapshot snapshot5; + + EXPECT_TRUE(snapshot1 == snapshot1); + EXPECT_TRUE(snapshot2 == snapshot2); + EXPECT_TRUE(snapshot3 == snapshot3); + EXPECT_TRUE(snapshot4 == snapshot4); + EXPECT_TRUE(snapshot5 == snapshot5); + + EXPECT_TRUE(snapshot1 != snapshot2); + EXPECT_TRUE(snapshot1 != snapshot3); + EXPECT_TRUE(snapshot1 != snapshot4); + EXPECT_TRUE(snapshot1 != snapshot5); + EXPECT_TRUE(snapshot2 != snapshot3); + EXPECT_TRUE(snapshot2 != snapshot4); + EXPECT_TRUE(snapshot2 != snapshot5); + EXPECT_TRUE(snapshot3 != snapshot4); + EXPECT_TRUE(snapshot3 != snapshot5); + EXPECT_TRUE(snapshot4 != snapshot5); + + EXPECT_FALSE(snapshot1 != snapshot1); + EXPECT_FALSE(snapshot2 != snapshot2); + EXPECT_FALSE(snapshot3 != snapshot3); + EXPECT_FALSE(snapshot4 != snapshot4); + EXPECT_FALSE(snapshot5 != snapshot5); + + EXPECT_FALSE(snapshot1 == snapshot2); + EXPECT_FALSE(snapshot1 == snapshot3); + EXPECT_FALSE(snapshot1 == snapshot4); + EXPECT_FALSE(snapshot1 == snapshot5); + EXPECT_FALSE(snapshot2 == snapshot3); + EXPECT_FALSE(snapshot2 == snapshot4); + EXPECT_FALSE(snapshot2 == snapshot5); + EXPECT_FALSE(snapshot3 == snapshot4); + EXPECT_FALSE(snapshot3 == snapshot5); + EXPECT_FALSE(snapshot4 == snapshot5); +} + +TEST_F(AggregateQuerySnapshotTest, + IdenticalSnapshotFromCollectionQueriesWithLimitShouldHaveSameHash) { + CollectionReference collection = + Collection({{"a", {{"k", FieldValue::String("a")}}}, + {"b", {{"k", FieldValue::String("b")}}}, + {"c", {{"k", FieldValue::String("c")}}}}); + AggregateQuerySnapshot snapshot1 = ReadAggregate(collection.Limit(1).Count()); + AggregateQuerySnapshot snapshot2 = ReadAggregate(collection.Limit(1).Count()); + + EXPECT_EQ(AggregateQuerySnapshotHash(snapshot1), + AggregateQuerySnapshotHash(snapshot1)); + EXPECT_EQ(AggregateQuerySnapshotHash(snapshot1), + AggregateQuerySnapshotHash(snapshot2)); +} + +TEST_F(AggregateQuerySnapshotTest, + IdenticalSnapshotFromCollectionQueriesShouldHaveSameHash) { + CollectionReference collection = + Collection({{"a", {{"k", FieldValue::String("a")}}}, + {"b", {{"k", FieldValue::String("b")}}}, + {"c", {{"k", FieldValue::String("c")}}}}); + AggregateQuerySnapshot snapshot1 = ReadAggregate(collection.Count()); + AggregateQuerySnapshot snapshot2 = ReadAggregate(collection.Count()); + + EXPECT_EQ(AggregateQuerySnapshotHash(snapshot1), + AggregateQuerySnapshotHash(snapshot1)); + EXPECT_EQ(AggregateQuerySnapshotHash(snapshot1), + AggregateQuerySnapshotHash(snapshot2)); +} + +TEST_F(AggregateQuerySnapshotTest, TestHashCode) { + CollectionReference collection = + Collection({{"a", {{"k", FieldValue::String("a")}}}, + {"b", {{"k", FieldValue::String("b")}}}, + {"c", {{"k", FieldValue::String("c")}}}}); + AggregateQuerySnapshot snapshot1 = ReadAggregate( + collection.WhereEqualTo("k", FieldValue::String("d")).Count()); + AggregateQuerySnapshot snapshot2 = ReadAggregate(collection.Limit(1).Count()); + AggregateQuerySnapshot snapshot3 = ReadAggregate(collection.Limit(3).Count()); + AggregateQuerySnapshot snapshot4 = ReadAggregate(collection.Count()); + + EXPECT_EQ(AggregateQuerySnapshotHash(snapshot1), + AggregateQuerySnapshotHash(snapshot1)); + EXPECT_NE(AggregateQuerySnapshotHash(snapshot1), + AggregateQuerySnapshotHash(snapshot2)); + EXPECT_NE(AggregateQuerySnapshotHash(snapshot1), + AggregateQuerySnapshotHash(snapshot3)); + EXPECT_NE(AggregateQuerySnapshotHash(snapshot1), + AggregateQuerySnapshotHash(snapshot4)); + EXPECT_EQ(AggregateQuerySnapshotHash(snapshot2), + AggregateQuerySnapshotHash(snapshot2)); + EXPECT_NE(AggregateQuerySnapshotHash(snapshot2), + AggregateQuerySnapshotHash(snapshot3)); + EXPECT_NE(AggregateQuerySnapshotHash(snapshot2), + AggregateQuerySnapshotHash(snapshot4)); + EXPECT_EQ(AggregateQuerySnapshotHash(snapshot3), + AggregateQuerySnapshotHash(snapshot3)); + EXPECT_NE(AggregateQuerySnapshotHash(snapshot3), + AggregateQuerySnapshotHash(snapshot4)); + EXPECT_EQ(AggregateQuerySnapshotHash(snapshot4), + AggregateQuerySnapshotHash(snapshot4)); +} + +} // namespace +} // namespace firestore +} // namespace firebase diff --git a/firestore/integration_test_internal/src/aggregate_query_test.cc b/firestore/integration_test_internal/src/aggregate_query_test.cc new file mode 100644 index 0000000000..4a52be9790 --- /dev/null +++ b/firestore/integration_test_internal/src/aggregate_query_test.cc @@ -0,0 +1,337 @@ +/* + * Copyright 2023 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. + */ + +#include "firebase/firestore.h" +#include "firestore_integration_test.h" + +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { + +size_t AggregateQueryHash(const AggregateQuery& aggregate_query) { + return aggregate_query.Hash(); +} + +namespace { + +using AggregateQueryTest = FirestoreIntegrationTest; + +TEST_F(AggregateQueryTest, DefaultConstructorReturnsInvalidObject) { + AggregateQuery aggregate_query; + EXPECT_EQ(aggregate_query.query(), Query()); + EXPECT_FALSE(aggregate_query.is_valid()); +} + +TEST_F(AggregateQueryTest, + CopyConstructorAppliedToValidObjectReturnsEqualObject) { + const Query query = TestFirestore()->Collection("foo").Limit(10); + const AggregateQuery aggregate_query = query.Count(); + + EXPECT_EQ(aggregate_query.query(), query); + EXPECT_TRUE(aggregate_query.is_valid()); + + AggregateQuery copied_aggregate_query(aggregate_query); + + EXPECT_EQ(aggregate_query.query(), query); + EXPECT_TRUE(aggregate_query.is_valid()); + + EXPECT_EQ(copied_aggregate_query.query(), query); + EXPECT_TRUE(copied_aggregate_query.is_valid()); +} + +TEST_F(AggregateQueryTest, CopyConstructorAppliedToDefaultReturnsEqualObject) { + const AggregateQuery aggregate_query; + + EXPECT_EQ(aggregate_query.query(), Query()); + EXPECT_FALSE(aggregate_query.is_valid()); + + AggregateQuery copied_aggregate_query(aggregate_query); + + EXPECT_EQ(aggregate_query.query(), Query()); + EXPECT_FALSE(aggregate_query.is_valid()); + + EXPECT_EQ(copied_aggregate_query.query(), Query()); + EXPECT_FALSE(copied_aggregate_query.is_valid()); +} + +TEST_F( + AggregateQueryTest, + DefaultObjectCopyAssignmentOperatorAppliedToValidObjectOperatorReturnsEqualObject) { + const Query query = TestFirestore()->Collection("foo").Limit(10); + const AggregateQuery aggregate_query = query.Count(); + + EXPECT_EQ(aggregate_query.query(), query); + EXPECT_TRUE(aggregate_query.is_valid()); + + AggregateQuery copied_aggregate_query; + + EXPECT_EQ(copied_aggregate_query.query(), Query()); + EXPECT_FALSE(copied_aggregate_query.is_valid()); + + copied_aggregate_query = aggregate_query; + + EXPECT_EQ(aggregate_query.query(), query); + EXPECT_TRUE(aggregate_query.is_valid()); + + EXPECT_EQ(copied_aggregate_query.query(), query); + EXPECT_TRUE(copied_aggregate_query.is_valid()); +} + +TEST_F( + AggregateQueryTest, + DefaultObjectCopyAssignmentOperatorAppliedToDefaultObjectReturnsEqualObject) { + const AggregateQuery aggregate_query; + + EXPECT_EQ(aggregate_query.query(), Query()); + EXPECT_FALSE(aggregate_query.is_valid()); + + AggregateQuery copied_aggregate_query; + + EXPECT_EQ(copied_aggregate_query.query(), Query()); + EXPECT_FALSE(copied_aggregate_query.is_valid()); + + copied_aggregate_query = aggregate_query; + + EXPECT_EQ(aggregate_query.query(), Query()); + EXPECT_FALSE(aggregate_query.is_valid()); + + EXPECT_EQ(copied_aggregate_query.query(), Query()); + EXPECT_FALSE(copied_aggregate_query.is_valid()); +} + +TEST_F(AggregateQueryTest, + ValidObjectCopyAssignmentAppliedToValidObjectReturnsEqualObject) { + const Query query1 = TestFirestore()->Collection("foo").Limit(10); + const Query query2 = TestFirestore()->Collection("bar").Limit(20); + const AggregateQuery aggregate_query = query1.Count(); + + EXPECT_EQ(aggregate_query.query(), query1); + EXPECT_TRUE(aggregate_query.is_valid()); + + AggregateQuery copied_aggregate_query = query2.Count(); + + EXPECT_EQ(copied_aggregate_query.query(), query2); + EXPECT_TRUE(copied_aggregate_query.is_valid()); + + copied_aggregate_query = aggregate_query; + + EXPECT_EQ(aggregate_query.query(), query1); + EXPECT_TRUE(aggregate_query.is_valid()); + + EXPECT_EQ(copied_aggregate_query.query(), query1); + EXPECT_TRUE(copied_aggregate_query.is_valid()); +} + +TEST_F(AggregateQueryTest, + ValidObjectCopyAssignmentAppliedToDefaultReturnsEqualObject) { + const AggregateQuery aggregate_query; + + EXPECT_EQ(aggregate_query.query(), Query()); + EXPECT_FALSE(aggregate_query.is_valid()); + + const Query query = TestFirestore()->Collection("foo").Limit(10); + AggregateQuery copied_aggregate_query = query.Count(); + + EXPECT_EQ(copied_aggregate_query.query(), query); + EXPECT_TRUE(copied_aggregate_query.is_valid()); + + copied_aggregate_query = aggregate_query; + + EXPECT_EQ(copied_aggregate_query.query(), Query()); + EXPECT_FALSE(copied_aggregate_query.is_valid()); + + EXPECT_EQ(aggregate_query.query(), Query()); + EXPECT_FALSE(aggregate_query.is_valid()); +} + +TEST_F(AggregateQueryTest, CopyAssignmentAppliedSelfReturnsEqualObject) { + const Query query = TestFirestore()->Collection("foo").Limit(10); + AggregateQuery aggregate_query = query.Count(); + + EXPECT_EQ(aggregate_query.query(), query); + EXPECT_TRUE(aggregate_query.is_valid()); + + aggregate_query = aggregate_query; + + EXPECT_EQ(aggregate_query.query(), query); + EXPECT_TRUE(aggregate_query.is_valid()); +} + +TEST_F(AggregateQueryTest, + CopyAssignmentAppliedToValidObjectReturnsEqualObject) { + const Query query = TestFirestore()->Collection("foo").Limit(10); + const AggregateQuery aggregate_query = query.Count(); + + EXPECT_EQ(aggregate_query.query(), query); + EXPECT_TRUE(aggregate_query.is_valid()); + + AggregateQuery copied_aggregate_query = aggregate_query; + + EXPECT_EQ(aggregate_query.query(), query); + EXPECT_TRUE(aggregate_query.is_valid()); + + EXPECT_EQ(copied_aggregate_query.query(), query); + EXPECT_TRUE(copied_aggregate_query.is_valid()); +} + +TEST_F(AggregateQueryTest, + MoveConstructorAppliedToValidObjectReturnsEqualObject) { + const Query query = TestFirestore()->Collection("foo").Limit(10); + AggregateQuery aggregate_query = query.Count(); + + EXPECT_EQ(aggregate_query.query(), query); + EXPECT_TRUE(aggregate_query.is_valid()); + + AggregateQuery moved_snapshot_dest(std::move(aggregate_query)); + + EXPECT_EQ(aggregate_query.query(), Query()); + EXPECT_FALSE(aggregate_query.is_valid()); + + EXPECT_EQ(moved_snapshot_dest.query(), query); + EXPECT_TRUE(moved_snapshot_dest.is_valid()); +} + +TEST_F(AggregateQueryTest, + MoveConstructorAppliedToDefaultObjectReturnsEqualObject) { + AggregateQuery aggregate_query; + + EXPECT_EQ(aggregate_query.query(), Query()); + EXPECT_FALSE(aggregate_query.is_valid()); + + AggregateQuery moved_snapshot_dest(std::move(aggregate_query)); + + EXPECT_EQ(aggregate_query.query(), Query()); + EXPECT_FALSE(aggregate_query.is_valid()); + + EXPECT_EQ(moved_snapshot_dest.query(), Query()); + EXPECT_FALSE(moved_snapshot_dest.is_valid()); +} + +TEST_F( + AggregateQueryTest, + DefaultObjectMoveAssignmentOperatorAppliedToValidObjectReturnsEqualObject) { + const Query query = TestFirestore()->Collection("foo").Limit(10); + AggregateQuery aggregate_query = query.Count(); + + EXPECT_EQ(aggregate_query.query(), query); + EXPECT_TRUE(aggregate_query.is_valid()); + + AggregateQuery snapshot_move_dest = std::move(aggregate_query); + + EXPECT_EQ(aggregate_query.query(), Query()); + EXPECT_FALSE(aggregate_query.is_valid()); + + EXPECT_EQ(snapshot_move_dest.query(), query); + EXPECT_TRUE(snapshot_move_dest.is_valid()); +} + +TEST_F( + AggregateQueryTest, + DefaultObjectMoveAssignmentOperatorAppliedToDefaultObjectReturnsEqualObject) { + AggregateQuery aggregate_query; + + EXPECT_EQ(aggregate_query.query(), Query()); + EXPECT_FALSE(aggregate_query.is_valid()); + + AggregateQuery snapshot_move_dest = std::move(aggregate_query); + + EXPECT_EQ(aggregate_query.query(), Query()); + EXPECT_FALSE(aggregate_query.is_valid()); + + EXPECT_EQ(snapshot_move_dest.query(), Query()); + EXPECT_FALSE(snapshot_move_dest.is_valid()); +} + +TEST_F( + AggregateQueryTest, + ValidObjectMoveAssignmentOperatorAppliedToValidObjectReturnsEqualObject) { + const Query query1 = TestFirestore()->Collection("foo").Limit(10); + const Query query2 = TestFirestore()->Collection("bar").Limit(20); + AggregateQuery aggregate_query = query1.Count(); + + EXPECT_EQ(aggregate_query.query(), query1); + EXPECT_TRUE(aggregate_query.is_valid()); + + AggregateQuery snapshot_move_dest = query2.Count(); + + EXPECT_EQ(snapshot_move_dest.query(), query2); + EXPECT_TRUE(snapshot_move_dest.is_valid()); + + snapshot_move_dest = std::move(aggregate_query); + + EXPECT_EQ(aggregate_query.query(), Query()); + EXPECT_FALSE(aggregate_query.is_valid()); + + EXPECT_EQ(snapshot_move_dest.query(), query1); + EXPECT_TRUE(snapshot_move_dest.is_valid()); +} + +TEST_F(AggregateQueryTest, + MoveAssignmentOperatorAppliedToSelfReturnsEqualObject) { + const Query query = TestFirestore()->Collection("foo").Limit(10); + + AggregateQuery aggregate_query = query.Count(); + + EXPECT_EQ(aggregate_query.query(), query); + EXPECT_TRUE(aggregate_query.is_valid()); + + aggregate_query = std::move(aggregate_query); + + EXPECT_EQ(aggregate_query.query(), query); + EXPECT_TRUE(aggregate_query.is_valid()); +} + +TEST_F( + AggregateQueryTest, + ValidObjectMoveAssignmentOperatorAppliedToDefaultObjectReturnsEqualObject) { + AggregateQuery aggregate_query; + + EXPECT_EQ(aggregate_query.query(), Query()); + EXPECT_FALSE(aggregate_query.is_valid()); + + const Query query = TestFirestore()->Collection("foo").Limit(10); + AggregateQuery snapshot_move_dest = query.Count(); + + EXPECT_EQ(snapshot_move_dest.query(), query); + EXPECT_TRUE(snapshot_move_dest.is_valid()); + + snapshot_move_dest = std::move(aggregate_query); + + EXPECT_EQ(aggregate_query.query(), Query()); + EXPECT_FALSE(aggregate_query.is_valid()); + + EXPECT_EQ(snapshot_move_dest.query(), Query()); + EXPECT_FALSE(snapshot_move_dest.is_valid()); +} + +TEST_F(AggregateQueryTest, TestHashCode) { + CollectionReference collection = + Collection({{"a", {{"k", FieldValue::String("a")}}}, + {"b", {{"k", FieldValue::String("b")}}}}); + Query query1 = + collection.Limit(2).OrderBy("sort", Query::Direction::kAscending); + Query query2 = + collection.Limit(2).OrderBy("sort", Query::Direction::kDescending); + EXPECT_NE(AggregateQueryHash(query1.Count()), + AggregateQueryHash(query2.Count())); + EXPECT_EQ(AggregateQueryHash(query1.Count()), + AggregateQueryHash(query1.Count())); +} + +} // namespace +} // namespace firestore +} // namespace firebase diff --git a/firestore/integration_test_internal/src/firestore_integration_test.cc b/firestore/integration_test_internal/src/firestore_integration_test.cc index ed6f2df358..df1854044f 100644 --- a/firestore/integration_test_internal/src/firestore_integration_test.cc +++ b/firestore/integration_test_internal/src/firestore_integration_test.cc @@ -255,6 +255,21 @@ QuerySnapshot FirestoreIntegrationTest::ReadDocuments( } } +AggregateQuerySnapshot FirestoreIntegrationTest::ReadAggregate( + const AggregateQuery& aggregate_query) const { + SCOPED_TRACE("FirestoreIntegrationTest::ReadAggregate()"); + Future future = + aggregate_query.Get(AggregateSource::kServer); + Stopwatch stopwatch; + const AggregateQuerySnapshot* result = Await(future); + stopwatch.stop(); + if (FailIfUnsuccessful("ReadAggregate", future, stopwatch)) { + return {}; + } else { + return *result; + } +} + void FirestoreIntegrationTest::DeleteDocument( DocumentReference reference) const { SCOPED_TRACE("FirestoreIntegrationTest::DeleteDocument(" + reference.path() + diff --git a/firestore/integration_test_internal/src/firestore_integration_test.h b/firestore/integration_test_internal/src/firestore_integration_test.h index af590fceea..249fbe3b0d 100644 --- a/firestore/integration_test_internal/src/firestore_integration_test.h +++ b/firestore/integration_test_internal/src/firestore_integration_test.h @@ -333,6 +333,10 @@ class FirestoreIntegrationTest : public testing::Test { // Read documents in the specified collection / query. QuerySnapshot ReadDocuments(const Query& reference) const; + // Read the aggregate. + AggregateQuerySnapshot ReadAggregate( + const AggregateQuery& aggregate_query) const; + // Delete the specified document. void DeleteDocument(DocumentReference reference) const; diff --git a/firestore/integration_test_internal/src/query_snapshot_test.cc b/firestore/integration_test_internal/src/query_snapshot_test.cc index 5c12235153..f1c93126b3 100644 --- a/firestore/integration_test_internal/src/query_snapshot_test.cc +++ b/firestore/integration_test_internal/src/query_snapshot_test.cc @@ -23,7 +23,6 @@ #include "firestore/src/android/query_snapshot_android.h" #endif // defined(__ANDROID__) -#include "gmock/gmock.h" #include "gtest/gtest.h" namespace firebase { @@ -49,94 +48,141 @@ TEST_F(QuerySnapshotTest, Assignment) { #endif // defined(__ANDROID__) -TEST_F(QuerySnapshotTest, Equality) { +TEST_F(QuerySnapshotTest, + IdenticalSnapshotFromCollectionQueriesWithLimitShouldBeEqual) { CollectionReference collection = Collection({{"a", {{"k", FieldValue::String("a")}}}, {"b", {{"k", FieldValue::String("b")}}}, {"c", {{"k", FieldValue::String("c")}}}}); QuerySnapshot snapshot1 = ReadDocuments(collection.Limit(2)); QuerySnapshot snapshot2 = ReadDocuments(collection.Limit(2)); - QuerySnapshot snapshot3 = ReadDocuments(collection.Limit(1)); - QuerySnapshot snapshot4 = ReadDocuments(collection); - QuerySnapshot snapshot5 = + + EXPECT_TRUE(snapshot1 == snapshot1); + EXPECT_TRUE(snapshot1 == snapshot2); + EXPECT_FALSE(snapshot1 != snapshot1); + EXPECT_FALSE(snapshot1 != snapshot2); +} + +TEST_F(QuerySnapshotTest, IdenticalDefaultSnapshotShouldBeEqual) { + QuerySnapshot snapshot1 = QuerySnapshot(); + QuerySnapshot snapshot2 = QuerySnapshot(); + + EXPECT_TRUE(snapshot1 == snapshot1); + EXPECT_TRUE(snapshot1 == snapshot2); + EXPECT_FALSE(snapshot1 != snapshot1); + EXPECT_FALSE(snapshot1 != snapshot2); +} + +TEST_F(QuerySnapshotTest, NonEquality) { + CollectionReference collection = + Collection({{"a", {{"k", FieldValue::String("a")}}}, + {"b", {{"k", FieldValue::String("b")}}}, + {"c", {{"k", FieldValue::String("c")}}}}); + QuerySnapshot snapshot1 = ReadDocuments(collection.Limit(2)); + QuerySnapshot snapshot2 = ReadDocuments(collection.Limit(1)); + QuerySnapshot snapshot3 = ReadDocuments(collection); + QuerySnapshot snapshot4 = ReadDocuments(collection.OrderBy("k", Query::Direction::kAscending)); - QuerySnapshot snapshot6 = + QuerySnapshot snapshot5 = ReadDocuments(collection.OrderBy("k", Query::Direction::kDescending)); - QuerySnapshot snapshot7 = QuerySnapshot(); - QuerySnapshot snapshot8 = QuerySnapshot(); + QuerySnapshot snapshot6 = QuerySnapshot(); EXPECT_TRUE(snapshot1 == snapshot1); - EXPECT_TRUE(snapshot1 == snapshot2); + EXPECT_TRUE(snapshot2 == snapshot2); + EXPECT_TRUE(snapshot3 == snapshot3); + EXPECT_TRUE(snapshot4 == snapshot4); + EXPECT_TRUE(snapshot5 == snapshot5); + + EXPECT_TRUE(snapshot1 != snapshot2); EXPECT_TRUE(snapshot1 != snapshot3); EXPECT_TRUE(snapshot1 != snapshot4); EXPECT_TRUE(snapshot1 != snapshot5); EXPECT_TRUE(snapshot1 != snapshot6); + EXPECT_TRUE(snapshot2 != snapshot3); + EXPECT_TRUE(snapshot2 != snapshot4); + EXPECT_TRUE(snapshot2 != snapshot5); + EXPECT_TRUE(snapshot2 != snapshot6); EXPECT_TRUE(snapshot3 != snapshot4); EXPECT_TRUE(snapshot3 != snapshot5); EXPECT_TRUE(snapshot3 != snapshot6); + EXPECT_TRUE(snapshot4 != snapshot5); + EXPECT_TRUE(snapshot4 != snapshot6); EXPECT_TRUE(snapshot5 != snapshot6); - EXPECT_TRUE(snapshot1 != snapshot7); - EXPECT_TRUE(snapshot2 != snapshot7); - EXPECT_TRUE(snapshot3 != snapshot7); - EXPECT_TRUE(snapshot4 != snapshot7); - EXPECT_TRUE(snapshot5 != snapshot7); - EXPECT_TRUE(snapshot6 != snapshot7); - EXPECT_TRUE(snapshot7 == snapshot8); EXPECT_FALSE(snapshot1 != snapshot1); - EXPECT_FALSE(snapshot1 != snapshot2); + EXPECT_FALSE(snapshot2 != snapshot2); + EXPECT_FALSE(snapshot3 != snapshot3); + EXPECT_FALSE(snapshot4 != snapshot4); + EXPECT_FALSE(snapshot5 != snapshot5); + + EXPECT_FALSE(snapshot1 == snapshot2); EXPECT_FALSE(snapshot1 == snapshot3); EXPECT_FALSE(snapshot1 == snapshot4); EXPECT_FALSE(snapshot1 == snapshot5); EXPECT_FALSE(snapshot1 == snapshot6); + EXPECT_FALSE(snapshot2 == snapshot3); + EXPECT_FALSE(snapshot2 == snapshot4); + EXPECT_FALSE(snapshot2 == snapshot5); + EXPECT_FALSE(snapshot2 == snapshot6); EXPECT_FALSE(snapshot3 == snapshot4); EXPECT_FALSE(snapshot3 == snapshot5); EXPECT_FALSE(snapshot3 == snapshot6); + EXPECT_FALSE(snapshot4 == snapshot5); + EXPECT_FALSE(snapshot4 == snapshot6); EXPECT_FALSE(snapshot5 == snapshot6); - EXPECT_FALSE(snapshot1 == snapshot7); - EXPECT_FALSE(snapshot2 == snapshot7); - EXPECT_FALSE(snapshot3 == snapshot7); - EXPECT_FALSE(snapshot4 == snapshot7); - EXPECT_FALSE(snapshot5 == snapshot7); - EXPECT_FALSE(snapshot6 == snapshot7); - EXPECT_FALSE(snapshot7 != snapshot8); } -TEST_F(QuerySnapshotTest, TestHashCode) { +TEST_F(QuerySnapshotTest, + IdenticalSnapshotFromCollectionQueriesWithLimitShouldHaveSameHash) { CollectionReference collection = Collection({{"a", {{"k", FieldValue::String("a")}}}, {"b", {{"k", FieldValue::String("b")}}}, {"c", {{"k", FieldValue::String("c")}}}}); QuerySnapshot snapshot1 = ReadDocuments(collection.Limit(2)); QuerySnapshot snapshot2 = ReadDocuments(collection.Limit(2)); - QuerySnapshot snapshot3 = ReadDocuments(collection.Limit(1)); - QuerySnapshot snapshot4 = ReadDocuments(collection); - QuerySnapshot snapshot5 = - ReadDocuments(collection.OrderBy("k", Query::Direction::kAscending)); - QuerySnapshot snapshot6 = - ReadDocuments(collection.OrderBy("k", Query::Direction::kDescending)); - QuerySnapshot snapshot7 = QuerySnapshot(); - QuerySnapshot snapshot8 = QuerySnapshot(); + EXPECT_EQ(QuerySnapshotHash(snapshot1), QuerySnapshotHash(snapshot1)); + EXPECT_EQ(QuerySnapshotHash(snapshot1), QuerySnapshotHash(snapshot2)); +} + +TEST_F(QuerySnapshotTest, IdenticalDefaultSnapshotShouldHaveSameHash) { + QuerySnapshot snapshot1 = QuerySnapshot(); + QuerySnapshot snapshot2 = QuerySnapshot(); EXPECT_EQ(QuerySnapshotHash(snapshot1), QuerySnapshotHash(snapshot1)); EXPECT_EQ(QuerySnapshotHash(snapshot1), QuerySnapshotHash(snapshot2)); +} + +TEST_F(QuerySnapshotTest, TestHashCodeNonEquality) { + CollectionReference collection = + Collection({{"a", {{"k", FieldValue::String("a")}}}, + {"b", {{"k", FieldValue::String("b")}}}, + {"c", {{"k", FieldValue::String("c")}}}}); + QuerySnapshot snapshot1 = ReadDocuments(collection.Limit(2)); + QuerySnapshot snapshot2 = ReadDocuments(collection.Limit(1)); + QuerySnapshot snapshot3 = ReadDocuments(collection); + QuerySnapshot snapshot4 = + ReadDocuments(collection.OrderBy("k", Query::Direction::kAscending)); + QuerySnapshot snapshot5 = + ReadDocuments(collection.OrderBy("k", Query::Direction::kDescending)); + QuerySnapshot snapshot6 = QuerySnapshot(); + + EXPECT_NE(QuerySnapshotHash(snapshot1), QuerySnapshotHash(snapshot2)); EXPECT_NE(QuerySnapshotHash(snapshot1), QuerySnapshotHash(snapshot3)); EXPECT_NE(QuerySnapshotHash(snapshot1), QuerySnapshotHash(snapshot4)); EXPECT_NE(QuerySnapshotHash(snapshot1), QuerySnapshotHash(snapshot5)); EXPECT_NE(QuerySnapshotHash(snapshot1), QuerySnapshotHash(snapshot6)); + EXPECT_NE(QuerySnapshotHash(snapshot2), QuerySnapshotHash(snapshot3)); + EXPECT_NE(QuerySnapshotHash(snapshot2), QuerySnapshotHash(snapshot4)); + EXPECT_NE(QuerySnapshotHash(snapshot2), QuerySnapshotHash(snapshot5)); + EXPECT_NE(QuerySnapshotHash(snapshot2), QuerySnapshotHash(snapshot6)); EXPECT_NE(QuerySnapshotHash(snapshot3), QuerySnapshotHash(snapshot4)); EXPECT_NE(QuerySnapshotHash(snapshot3), QuerySnapshotHash(snapshot5)); EXPECT_NE(QuerySnapshotHash(snapshot3), QuerySnapshotHash(snapshot6)); + EXPECT_NE(QuerySnapshotHash(snapshot4), QuerySnapshotHash(snapshot5)); + EXPECT_NE(QuerySnapshotHash(snapshot4), QuerySnapshotHash(snapshot6)); EXPECT_NE(QuerySnapshotHash(snapshot5), QuerySnapshotHash(snapshot6)); - EXPECT_NE(QuerySnapshotHash(snapshot1), QuerySnapshotHash(snapshot7)); - EXPECT_NE(QuerySnapshotHash(snapshot2), QuerySnapshotHash(snapshot7)); - EXPECT_NE(QuerySnapshotHash(snapshot3), QuerySnapshotHash(snapshot7)); - EXPECT_NE(QuerySnapshotHash(snapshot4), QuerySnapshotHash(snapshot7)); - EXPECT_NE(QuerySnapshotHash(snapshot5), QuerySnapshotHash(snapshot7)); - EXPECT_NE(QuerySnapshotHash(snapshot6), QuerySnapshotHash(snapshot7)); - EXPECT_EQ(QuerySnapshotHash(snapshot7), QuerySnapshotHash(snapshot8)); } } // namespace firestore diff --git a/firestore/src/android/aggregate_query_android.cc b/firestore/src/android/aggregate_query_android.cc new file mode 100644 index 0000000000..d9d6da0144 --- /dev/null +++ b/firestore/src/android/aggregate_query_android.cc @@ -0,0 +1,75 @@ +/* + * Copyright 2023 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. + */ + +#include "firestore/src/android/aggregate_query_android.h" + +#include "firestore/src/android/aggregate_source_android.h" +#include "firestore/src/jni/compare.h" +#include "firestore/src/jni/env.h" +#include "firestore/src/jni/loader.h" +#include "firestore/src/jni/task.h" + +namespace firebase { +namespace firestore { +namespace { + +using jni::Env; +using jni::Local; +using jni::Method; +using jni::Object; +using jni::Task; + +constexpr char kClassName[] = + PROGUARD_KEEP_CLASS "com/google/firebase/firestore/AggregateQuery"; +Method kGet("get", + "(Lcom/google/firebase/firestore/AggregateSource;)" + "Lcom/google/android/gms/tasks/Task;"); +Method kGetQuery("getQuery", "()Lcom/google/firebase/firestore/Query;"); +Method kHashCode("hashCode", "()I"); + +} // namespace + +void AggregateQueryInternal::Initialize(jni::Loader& loader) { + loader.LoadClass(kClassName, kGet, kGetQuery, kHashCode); +} + +Query AggregateQueryInternal::query() const { + Env env = GetEnv(); + Local query = env.Call(obj_, kGetQuery); + return firestore_->NewQuery(env, query); +} + +Future AggregateQueryInternal::Get( + AggregateSource aggregate_source) { + Env env = GetEnv(); + Local java_source = + AggregateSourceInternal::Create(env, aggregate_source); + Local task = env.Call(obj_, kGet, java_source); + return promises_.NewFuture(env, AsyncFn::kGet, task); +} + +std::size_t AggregateQueryInternal::Hash() const { + Env env = GetEnv(); + return env.Call(obj_, kHashCode); +} + +bool operator==(const AggregateQueryInternal& lhs, + const AggregateQueryInternal& rhs) { + return jni::EqualityCompareJni(lhs, rhs); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/android/aggregate_query_android.h b/firestore/src/android/aggregate_query_android.h new file mode 100644 index 0000000000..7545d62f72 --- /dev/null +++ b/firestore/src/android/aggregate_query_android.h @@ -0,0 +1,78 @@ +/* + * Copyright 2023 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. + */ + +#ifndef FIREBASE_FIRESTORE_SRC_ANDROID_AGGREGATE_QUERY_ANDROID_H_ +#define FIREBASE_FIRESTORE_SRC_ANDROID_AGGREGATE_QUERY_ANDROID_H_ + +#include "firestore/src/android/promise_factory_android.h" +#include "firestore/src/android/query_android.h" +#include "firestore/src/android/wrapper.h" +#include "firestore/src/include/firebase/firestore/aggregate_source.h" + +namespace firebase { +namespace firestore { + +class AggregateQueryInternal : public Wrapper { + public: + // Each API of AggregateQuery that returns a Future needs to define an enum + // value here. For example, a Future-returning method Foo() relies on the enum + // value kFoo. The enum values are used to identify and manage Future in the + // Firestore Future manager. + enum class AsyncFn { + kGet = 0, + kCount, // Must be the last enum value. + }; + + static void Initialize(jni::Loader& loader); + + AggregateQueryInternal(FirestoreInternal* firestore, + const jni::Object& object) + : Wrapper(firestore, object), promises_(firestore) {} + + /** + * @brief Returns the query whose aggregations will be calculated by this + * object. + */ + Query query() const; + + /** + * @brief Executes the aggregate query and returns the results as a + * AggregateQuerySnapshot. + * + * @param[in] aggregate_source A value to configure the get behavior. + * + * @return A Future that will be resolved with the results of the + * AggregateQuery. + */ + Future Get(AggregateSource aggregate_source); + + size_t Hash() const; + + private: + PromiseFactory promises_; +}; + +bool operator==(const AggregateQueryInternal& lhs, + const AggregateQueryInternal& rhs); +inline bool operator!=(const AggregateQueryInternal& lhs, + const AggregateQueryInternal& rhs) { + return !(lhs == rhs); +} + +} // namespace firestore +} // namespace firebase + +#endif // FIREBASE_FIRESTORE_SRC_ANDROID_AGGREGATE_QUERY_ANDROID_H_ diff --git a/firestore/src/android/aggregate_query_snapshot_android.cc b/firestore/src/android/aggregate_query_snapshot_android.cc new file mode 100644 index 0000000000..ddb91c80e2 --- /dev/null +++ b/firestore/src/android/aggregate_query_snapshot_android.cc @@ -0,0 +1,79 @@ +/* + * Copyright 2023 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. + */ + +#include "firestore/src/android/aggregate_query_snapshot_android.h" + +#include "firestore/src/android/aggregate_query_android.h" +#include "firestore/src/jni/compare.h" +#include "firestore/src/jni/env.h" +#include "firestore/src/jni/loader.h" + +namespace firebase { +namespace firestore { +namespace { + +using jni::Constructor; +using jni::Env; +using jni::Local; +using jni::Method; +using jni::Object; + +constexpr char kClassName[] = + PROGUARD_KEEP_CLASS "com/google/firebase/firestore/AggregateQuerySnapshot"; +Constructor kConstructor( + "(Lcom/google/firebase/firestore/AggregateQuery;J)V"); +Method kGetCount("getCount", "()J"); +Method kGetQuery("getQuery", + "()Lcom/google/firebase/firestore/AggregateQuery;"); +Method kHashCode("hashCode", "()I"); + +} // namespace + +void AggregateQuerySnapshotInternal::Initialize(jni::Loader& loader) { + loader.LoadClass(kClassName, kConstructor, kGetCount, kGetQuery, kHashCode); +} + +AggregateQuerySnapshot AggregateQuerySnapshotInternal::Create( + Env& env, AggregateQueryInternal& aggregate_query_internal, int64_t count) { + Local instance = + env.New(kConstructor, aggregate_query_internal.ToJava(), count); + return aggregate_query_internal.firestore_internal() + ->NewAggregateQuerySnapshot(env, instance); +} + +AggregateQuery AggregateQuerySnapshotInternal::query() const { + Env env = GetEnv(); + Local query = env.Call(obj_, kGetQuery); + return firestore_->NewAggregateQuery(env, query); +} + +int64_t AggregateQuerySnapshotInternal::count() const { + Env env = GetEnv(); + return env.Call(obj_, kGetCount); +} + +std::size_t AggregateQuerySnapshotInternal::Hash() const { + Env env = GetEnv(); + return env.Call(obj_, kHashCode); +} + +bool operator==(const AggregateQuerySnapshotInternal& lhs, + const AggregateQuerySnapshotInternal& rhs) { + return jni::EqualityCompareJni(lhs, rhs); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/android/aggregate_query_snapshot_android.h b/firestore/src/android/aggregate_query_snapshot_android.h new file mode 100644 index 0000000000..6ebad41a3e --- /dev/null +++ b/firestore/src/android/aggregate_query_snapshot_android.h @@ -0,0 +1,58 @@ +/* + * Copyright 2023 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. + */ + +#ifndef FIREBASE_FIRESTORE_SRC_ANDROID_AGGREGATE_QUERY_SNAPSHOT_ANDROID_H_ +#define FIREBASE_FIRESTORE_SRC_ANDROID_AGGREGATE_QUERY_SNAPSHOT_ANDROID_H_ + +#include "firestore/src/android/wrapper.h" +#include "firestore/src/include/firebase/firestore/aggregate_query.h" +#include "firestore/src/include/firebase/firestore/aggregate_query_snapshot.h" + +namespace firebase { +namespace firestore { + +class AggregateQuery; + +class AggregateQuerySnapshotInternal : public Wrapper { + public: + using Wrapper::Wrapper; + + static void Initialize(jni::Loader& loader); + + AggregateQuery query() const; + int64_t count() const; + + std::size_t Hash() const; + + private: + friend class AggregateQuerySnapshotTest; + static AggregateQuerySnapshot Create( + jni::Env& env, + AggregateQueryInternal& aggregate_query_internal, + int64_t count); +}; + +bool operator==(const AggregateQuerySnapshotInternal& lhs, + const AggregateQuerySnapshotInternal& rhs); +inline bool operator!=(const AggregateQuerySnapshotInternal& lhs, + const AggregateQuerySnapshotInternal& rhs) { + return !(lhs == rhs); +} + +} // namespace firestore +} // namespace firebase + +#endif // FIREBASE_FIRESTORE_SRC_ANDROID_AGGREGATE_QUERY_SNAPSHOT_ANDROID_H_ diff --git a/firestore/src/android/aggregate_source_android.cc b/firestore/src/android/aggregate_source_android.cc new file mode 100644 index 0000000000..c65e8d66ad --- /dev/null +++ b/firestore/src/android/aggregate_source_android.cc @@ -0,0 +1,51 @@ +/* + * Copyright 2023 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. + */ + +#include "firestore/src/android/aggregate_source_android.h" + +#include "firestore/src/jni/env.h" +#include "firestore/src/jni/loader.h" + +namespace firebase { +namespace firestore { +namespace { + +using jni::Env; +using jni::Local; +using jni::Object; +using jni::StaticField; + +constexpr char kClass[] = + PROGUARD_KEEP_CLASS "com/google/firebase/firestore/AggregateSource"; +StaticField kServer("SERVER", + "Lcom/google/firebase/firestore/AggregateSource;"); + +} // namespace + +void AggregateSourceInternal::Initialize(jni::Loader& loader) { + loader.LoadClass(kClass, kServer); +} + +Local AggregateSourceInternal::Create( + Env& env, AggregateSource aggregate_source) { + switch (aggregate_source) { + case AggregateSource::kServer: + return env.Get(kServer); + } +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/android/aggregate_source_android.h b/firestore/src/android/aggregate_source_android.h new file mode 100644 index 0000000000..9db5d35477 --- /dev/null +++ b/firestore/src/android/aggregate_source_android.h @@ -0,0 +1,36 @@ +/* + * Copyright 2023 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. + */ + +#ifndef FIREBASE_FIRESTORE_SRC_ANDROID_AGGREGATE_SOURCE_ANDROID_H_ +#define FIREBASE_FIRESTORE_SRC_ANDROID_AGGREGATE_SOURCE_ANDROID_H_ + +#include "firestore/src/include/firebase/firestore/aggregate_source.h" +#include "firestore/src/jni/jni_fwd.h" + +namespace firebase { +namespace firestore { + +class AggregateSourceInternal { + public: + static void Initialize(jni::Loader& loader); + + static jni::Local Create(jni::Env& env, AggregateSource source); +}; + +} // namespace firestore +} // namespace firebase + +#endif // FIREBASE_FIRESTORE_SRC_ANDROID_AGGREGATE_SOURCE_ANDROID_H_ diff --git a/firestore/src/android/firestore_android.cc b/firestore/src/android/firestore_android.cc index 62917accd8..4b9926ce09 100644 --- a/firestore/src/android/firestore_android.cc +++ b/firestore/src/android/firestore_android.cc @@ -22,6 +22,9 @@ #include "app/src/include/firebase/future.h" #include "app/src/reference_counted_future_impl.h" #include "firestore/firestore_resources.h" +#include "firestore/src/android/aggregate_query_android.h" +#include "firestore/src/android/aggregate_query_snapshot_android.h" +#include "firestore/src/android/aggregate_source_android.h" #include "firestore/src/android/blob_android.h" #include "firestore/src/android/collection_reference_android.h" #include "firestore/src/android/converter_android.h" @@ -318,6 +321,9 @@ bool FirestoreInternal::Initialize(App* app) { InitializeFirestoreTasks(loader); InitializeUserCallbackExecutor(loader); + AggregateQueryInternal::Initialize(loader); + AggregateQuerySnapshotInternal::Initialize(loader); + AggregateSourceInternal::Initialize(loader); BlobInternal::Initialize(loader); CollectionReferenceInternal::Initialize(loader); DirectionInternal::Initialize(loader); @@ -620,6 +626,17 @@ Env FirestoreInternal::GetEnv() { return env; } +AggregateQuery FirestoreInternal::NewAggregateQuery( + Env& env, const jni::Object& aggregate_query) const { + return MakePublic(env, mutable_this(), aggregate_query); +} + +AggregateQuerySnapshot FirestoreInternal::NewAggregateQuerySnapshot( + Env& env, const jni::Object& aggregate_query_snapshot) const { + return MakePublic(env, mutable_this(), + aggregate_query_snapshot); +} + CollectionReference FirestoreInternal::NewCollectionReference( Env& env, const jni::Object& reference) const { return MakePublic(env, mutable_this(), reference); diff --git a/firestore/src/android/firestore_android.h b/firestore/src/android/firestore_android.h index 5c5367a8f5..d5079c7533 100644 --- a/firestore/src/android/firestore_android.h +++ b/firestore/src/android/firestore_android.h @@ -144,6 +144,12 @@ class FirestoreInternal { static jni::Env GetEnv(); + AggregateQuery NewAggregateQuery(jni::Env& env, + const jni::Object& aggregate_query) const; + + AggregateQuerySnapshot NewAggregateQuerySnapshot( + jni::Env& env, const jni::Object& aggregate_query_snapshot) const; + CollectionReference NewCollectionReference( jni::Env& env, const jni::Object& reference) const; DocumentReference NewDocumentReference(jni::Env& env, diff --git a/firestore/src/android/load_bundle_task_progress_android.cc b/firestore/src/android/load_bundle_task_progress_android.cc index 1593a4a022..00a0005249 100644 --- a/firestore/src/android/load_bundle_task_progress_android.cc +++ b/firestore/src/android/load_bundle_task_progress_android.cc @@ -25,7 +25,6 @@ namespace firestore { namespace { using jni::Class; -using jni::Constructor; using jni::Env; using jni::Local; using jni::Method; diff --git a/firestore/src/android/promise_android.h b/firestore/src/android/promise_android.h index 37df349945..b1f9c6ec5e 100644 --- a/firestore/src/android/promise_android.h +++ b/firestore/src/android/promise_android.h @@ -22,6 +22,7 @@ #include "app/src/reference_counted_future_impl.h" #include "app/src/util_android.h" +#include "firestore/src/android/aggregate_query_snapshot_android.h" #include "firestore/src/android/converter_android.h" #include "firestore/src/android/document_snapshot_android.h" #include "firestore/src/android/exception_android.h" diff --git a/firestore/src/android/query_android.cc b/firestore/src/android/query_android.cc index b28de7eb81..81fdd418e0 100644 --- a/firestore/src/android/query_android.cc +++ b/firestore/src/android/query_android.cc @@ -51,6 +51,8 @@ using jni::Task; constexpr char kClassName[] = PROGUARD_KEEP_CLASS "com/google/firebase/firestore/Query"; +Method kCount("count", + "()Lcom/google/firebase/firestore/AggregateQuery;"); Method kEqualTo( "whereEqualTo", "(Lcom/google/firebase/firestore/FieldPath;Ljava/lang/Object;)" @@ -136,7 +138,7 @@ Method kHashCode("hashCode", "()I"); void QueryInternal::Initialize(jni::Loader& loader) { loader.LoadClass( - kClassName, kEqualTo, kNotEqualTo, kLessThan, kLessThanOrEqualTo, + kClassName, kCount, kEqualTo, kNotEqualTo, kLessThan, kLessThanOrEqualTo, kGreaterThan, kGreaterThanOrEqualTo, kArrayContains, kArrayContainsAny, kIn, kNotIn, kOrderBy, kLimit, kLimitToLast, kStartAtSnapshot, kStartAt, kStartAfterSnapshot, kStartAfter, kEndBeforeSnapshot, kEndBefore, @@ -148,6 +150,12 @@ Firestore* QueryInternal::firestore() { return firestore_->firestore_public(); } +AggregateQuery QueryInternal::Count() const { + Env env = GetEnv(); + Local aggregate_query = env.Call(obj_, kCount); + return firestore_->NewAggregateQuery(env, aggregate_query); +} + Query QueryInternal::WhereEqualTo(const FieldPath& field, const FieldValue& value) const { return Where(field, kEqualTo, value); diff --git a/firestore/src/android/query_android.h b/firestore/src/android/query_android.h index 34b7ad4bee..abb3dea8e3 100644 --- a/firestore/src/android/query_android.h +++ b/firestore/src/android/query_android.h @@ -55,6 +55,22 @@ class QueryInternal : public Wrapper { /** Gets the Firestore instance associated with this query. */ Firestore* firestore(); + /** + * @brief Returns a query that counts the documents in the result set of this + * query. + * + * The returned query, when executed, counts the documents in the result set + * of this query without actually downloading the documents. + * + * Using the returned query to count the documents is efficient because only + * the final count, not the documents' data, is downloaded. The returned query + * can even count the documents if the result set would be prohibitively large + * to download entirely (e.g. thousands of documents). + * + * @return A query that counts the documents in the result set of this query. + */ + virtual AggregateQuery Count() const; + /** * @brief Creates and returns a new Query with the additional filter that * documents must contain the specified field and the value should be equal to diff --git a/firestore/src/common/aggregate_query.cc b/firestore/src/common/aggregate_query.cc new file mode 100644 index 0000000000..043af1748f --- /dev/null +++ b/firestore/src/common/aggregate_query.cc @@ -0,0 +1,113 @@ +/* + * Copyright 2023 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. + */ + +#include "firestore/src/include/firebase/firestore/aggregate_query.h" +#include "firestore/src/common/cleanup.h" +#include "firestore/src/common/futures.h" +#include "firestore/src/common/hard_assert_common.h" +#include "firestore/src/common/util.h" +#include "firestore/src/include/firebase/firestore/aggregate_query_snapshot.h" + +#if defined(__ANDROID__) +#include "firestore/src/android/aggregate_query_android.h" +#else +#include "firestore/src/main/aggregate_query_main.h" +#endif // defined(__ANDROID__) + +namespace firebase { +namespace firestore { + +using CleanupFnAggregateQuery = CleanupFn; + +AggregateQuery::AggregateQuery() {} + +AggregateQuery::AggregateQuery(const AggregateQuery& other) { + if (other.internal_) { + internal_ = new AggregateQueryInternal(*other.internal_); + } + CleanupFnAggregateQuery::Register(this, internal_); +} + +AggregateQuery::AggregateQuery(AggregateQuery&& other) { + CleanupFnAggregateQuery::Unregister(&other, other.internal_); + std::swap(internal_, other.internal_); + CleanupFnAggregateQuery::Register(this, internal_); +} + +AggregateQuery::AggregateQuery(AggregateQueryInternal* internal) + : internal_(internal) { + SIMPLE_HARD_ASSERT(internal != nullptr); + CleanupFnAggregateQuery::Register(this, internal_); +} + +AggregateQuery::~AggregateQuery() { + CleanupFnAggregateQuery::Unregister(this, internal_); + delete internal_; + internal_ = nullptr; +} + +AggregateQuery& AggregateQuery::operator=(const AggregateQuery& other) { + if (this == &other) { + return *this; + } + + CleanupFnAggregateQuery::Unregister(this, internal_); + delete internal_; + if (other.internal_) { + internal_ = new AggregateQueryInternal(*other.internal_); + } else { + internal_ = nullptr; + } + CleanupFnAggregateQuery::Register(this, internal_); + return *this; +} + +AggregateQuery& AggregateQuery::operator=(AggregateQuery&& other) { + if (this == &other) { + return *this; + } + + CleanupFnAggregateQuery::Unregister(&other, other.internal_); + CleanupFnAggregateQuery::Unregister(this, internal_); + delete internal_; + internal_ = other.internal_; + other.internal_ = nullptr; + CleanupFnAggregateQuery::Register(this, internal_); + return *this; +} + +Query AggregateQuery::query() const { + if (!internal_) return {}; + return internal_->query(); +} + +Future AggregateQuery::Get( + AggregateSource aggregate_source) const { + if (!internal_) return FailedFuture(); + return internal_->Get(aggregate_source); +} + +size_t AggregateQuery::Hash() const { + if (!internal_) return {}; + return internal_->Hash(); +} + +bool operator==(const AggregateQuery& lhs, const AggregateQuery& rhs) { + return EqualityCompare(lhs.internal_, rhs.internal_); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/common/aggregate_query_snapshot.cc b/firestore/src/common/aggregate_query_snapshot.cc new file mode 100644 index 0000000000..a4ce874f33 --- /dev/null +++ b/firestore/src/common/aggregate_query_snapshot.cc @@ -0,0 +1,117 @@ +/* + * Copyright 2023 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. + */ + +#include "firestore/src/include/firebase/firestore/aggregate_query_snapshot.h" + +#include "firestore/src/common/cleanup.h" +#include "firestore/src/common/hard_assert_common.h" +#include "firestore/src/common/util.h" +#include "firestore/src/include/firebase/firestore/aggregate_query.h" + +#if defined(__ANDROID__) +#include "firestore/src/android/aggregate_query_snapshot_android.h" +#else +#include "firestore/src/main/aggregate_query_snapshot_main.h" +#endif // defined(__ANDROID__) + +namespace firebase { +namespace firestore { + +using CleanupFnAggregateQuerySnapshot = CleanupFn; + +AggregateQuerySnapshot::AggregateQuerySnapshot() {} + +AggregateQuerySnapshot::AggregateQuerySnapshot( + const AggregateQuerySnapshot& other) { + if (other.internal_) { + internal_ = new AggregateQuerySnapshotInternal(*other.internal_); + } + CleanupFnAggregateQuerySnapshot::Register(this, internal_); +} + +AggregateQuerySnapshot::AggregateQuerySnapshot(AggregateQuerySnapshot&& other) { + CleanupFnAggregateQuerySnapshot::Unregister(&other, other.internal_); + std::swap(internal_, other.internal_); + CleanupFnAggregateQuerySnapshot::Register(this, internal_); +} + +AggregateQuerySnapshot::AggregateQuerySnapshot( + AggregateQuerySnapshotInternal* internal) + : internal_(internal) { + SIMPLE_HARD_ASSERT(internal != nullptr); + CleanupFnAggregateQuerySnapshot::Register(this, internal_); +} + +AggregateQuerySnapshot::~AggregateQuerySnapshot() { + CleanupFnAggregateQuerySnapshot::Unregister(this, internal_); + delete internal_; + internal_ = nullptr; +} + +AggregateQuerySnapshot& AggregateQuerySnapshot::operator=( + const AggregateQuerySnapshot& other) { + if (this == &other) { + return *this; + } + + CleanupFnAggregateQuerySnapshot::Unregister(this, internal_); + delete internal_; + if (other.internal_) { + internal_ = new AggregateQuerySnapshotInternal(*other.internal_); + } else { + internal_ = nullptr; + } + CleanupFnAggregateQuerySnapshot::Register(this, internal_); + return *this; +} + +AggregateQuerySnapshot& AggregateQuerySnapshot::operator=( + AggregateQuerySnapshot&& other) { + if (this == &other) { + return *this; + } + + CleanupFnAggregateQuerySnapshot::Unregister(&other, other.internal_); + CleanupFnAggregateQuerySnapshot::Unregister(this, internal_); + delete internal_; + internal_ = other.internal_; + other.internal_ = nullptr; + CleanupFnAggregateQuerySnapshot::Register(this, internal_); + return *this; +} + +AggregateQuery AggregateQuerySnapshot::query() const { + if (!internal_) return {}; + return internal_->query(); +} + +int64_t AggregateQuerySnapshot::count() const { + if (!internal_) return 0; + return internal_->count(); +} + +bool operator==(const AggregateQuerySnapshot& lhs, + const AggregateQuerySnapshot& rhs) { + return EqualityCompare(lhs.internal_, rhs.internal_); +} + +size_t AggregateQuerySnapshot::Hash() const { + if (!internal_) return {}; + return internal_->Hash(); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/common/query.cc b/firestore/src/common/query.cc index de525c52cc..7edbdd7602 100644 --- a/firestore/src/common/query.cc +++ b/firestore/src/common/query.cc @@ -23,6 +23,7 @@ #include "firestore/src/common/futures.h" #include "firestore/src/common/hard_assert_common.h" #include "firestore/src/common/util.h" +#include "firestore/src/include/firebase/firestore/aggregate_query.h" #include "firestore/src/include/firebase/firestore/document_snapshot.h" #include "firestore/src/include/firebase/firestore/field_path.h" #include "firestore/src/include/firebase/firestore/field_value.h" @@ -106,6 +107,11 @@ Firestore* Query::firestore() { return internal_->firestore(); } +AggregateQuery Query::Count() const { + if (!internal_) return {}; + return internal_->Count(); +} + Query Query::WhereEqualTo(const std::string& field, const FieldValue& value) const { return WhereEqualTo(FieldPath::FromDotSeparatedString(field), value); diff --git a/firestore/src/common/type_mapping.h b/firestore/src/common/type_mapping.h index aedee57887..269b92a410 100644 --- a/firestore/src/common/type_mapping.h +++ b/firestore/src/common/type_mapping.h @@ -22,6 +22,10 @@ namespace firebase { namespace firestore { +class AggregateQuery; +class AggregateQueryInternal; +class AggregateQuerySnapshot; +class AggregateQuerySnapshotInternal; class CollectionReference; class CollectionReferenceInternal; class DocumentChange; @@ -54,6 +58,14 @@ class WriteBatchInternal; template struct InternalTypeMap {}; +template <> +struct InternalTypeMap { + using type = AggregateQueryInternal; +}; +template <> +struct InternalTypeMap { + using type = AggregateQuerySnapshotInternal; +}; template <> struct InternalTypeMap { using type = CollectionReferenceInternal; diff --git a/firestore/src/include/firebase/firestore.h b/firestore/src/include/firebase/firestore.h index 3b1da17d83..d8a461e7b2 100644 --- a/firestore/src/include/firebase/firestore.h +++ b/firestore/src/include/firebase/firestore.h @@ -28,6 +28,9 @@ #include "firebase/log.h" // Include *all* the public headers to make sure including just "firestore.h" is // sufficient for users. +#include "firebase/firestore/aggregate_query.h" +#include "firebase/firestore/aggregate_query_snapshot.h" +#include "firebase/firestore/aggregate_source.h" #include "firebase/firestore/collection_reference.h" #include "firebase/firestore/document_change.h" #include "firebase/firestore/document_reference.h" diff --git a/firestore/src/include/firebase/firestore/aggregate_query.h b/firestore/src/include/firebase/firestore/aggregate_query.h new file mode 100644 index 0000000000..035d923af7 --- /dev/null +++ b/firestore/src/include/firebase/firestore/aggregate_query.h @@ -0,0 +1,158 @@ +/* + * Copyright 2023 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. + */ + +#ifndef FIREBASE_FIRESTORE_SRC_INCLUDE_FIREBASE_FIRESTORE_AGGREGATE_QUERY_H_ +#define FIREBASE_FIRESTORE_SRC_INCLUDE_FIREBASE_FIRESTORE_AGGREGATE_QUERY_H_ + +#include "firebase/firestore/aggregate_source.h" + +#include + +namespace firebase { +/// @cond FIREBASE_APP_INTERNAL +template +class Future; +/// @endcond + +namespace firestore { + +class AggregateQueryInternal; +class AggregateQuerySnapshot; +class Query; + +/** + * @brief A query that calculates aggregations over an underlying query. + */ +class AggregateQuery { + public: + /** + * @brief Creates an invalid AggregateQuery that has to be reassigned before + * it can be used. + * + * Calling any member function on an invalid AggregateQuery will be a no-op. + * If the function returns a value, it will return a zero, empty, or invalid + * value, depending on the type of the value. + */ + AggregateQuery(); + + /** + * @brief Copy constructor. + * + * `AggregateQuery` is immutable and can be efficiently copied (no deep copy + * is performed). + * + * @param[in] other `AggregateQuery` to copy from. + */ + AggregateQuery(const AggregateQuery& other); + + /** + * @brief Move constructor. + * + * Moving is more efficient than copying for a `AggregateQuery`. After being + * moved from, a `AggregateQuery` is equivalent to its default-constructed + * state. + * + * @param[in] other `AggregateQuery` to move data from. + */ + AggregateQuery(AggregateQuery&& other); + + virtual ~AggregateQuery(); + + /** + * @brief Copy assignment operator. + * + * `AggregateQuery` is immutable and can be efficiently copied (no deep copy + * is performed). + * + * @param[in] other `AggregateQuery` to copy from. + * + * @return Reference to the destination `AggregateQuery`. + */ + AggregateQuery& operator=(const AggregateQuery& other); + + /** + * @brief Move assignment operator. + * + * Moving is more efficient than copying for a `AggregateQuery`. After being + * moved from, a `AggregateQuery` is equivalent to its default-constructed + * state. + * + * @param[in] other `AggregateQuery` to move data from. + * + * @return Reference to the destination `AggregateQuery`. + */ + AggregateQuery& operator=(AggregateQuery&& other); + + /** + * @brief Returns the query whose aggregations will be calculated by this + * object. + */ + virtual Query query() const; + + /** + * @brief Executes this query. + * + * @param[in] aggregate_source The source from which to acquire the aggregate + * results. + * + * @return A Future that will be resolved with the results of the + * AggregateQuery. + */ + virtual Future Get( + AggregateSource aggregate_source) const; + + /** + * @brief Returns true if this `AggregateQuery` is valid, false if it is not + * valid. An invalid `AggregateQuery` could be the result of: + * - Creating a `AggregateQuery` using the default constructor. + * - Moving from the `AggregateQuery`. + * - Deleting your Firestore instance, which will invalidate all the + * `AggregateQuery` instances associated with it. + * + * @return true if this `AggregateQuery` is valid, false if this + * `AggregateQuery` is invalid. + */ + bool is_valid() const { return internal_ != nullptr; } + + private: + std::size_t Hash() const; + + friend class AggregateQueryInternal; + friend struct ConverterImpl; + + friend bool operator==(const AggregateQuery& lhs, const AggregateQuery& rhs); + friend std::size_t AggregateQueryHash(const AggregateQuery& aggregate_query); + + template + friend struct CleanupFn; + + explicit AggregateQuery(AggregateQueryInternal* internal); + + mutable AggregateQueryInternal* internal_ = nullptr; +}; + +/** Checks `lhs` and `rhs` for equality. */ +bool operator==(const AggregateQuery& lhs, const AggregateQuery& rhs); + +/** Checks `lhs` and `rhs` for inequality. */ +inline bool operator!=(const AggregateQuery& lhs, const AggregateQuery& rhs) { + return !(lhs == rhs); +} + +} // namespace firestore +} // namespace firebase + +#endif // FIREBASE_FIRESTORE_SRC_INCLUDE_FIREBASE_FIRESTORE_AGGREGATE_QUERY_H_ diff --git a/firestore/src/include/firebase/firestore/aggregate_query_snapshot.h b/firestore/src/include/firebase/firestore/aggregate_query_snapshot.h new file mode 100644 index 0000000000..3ecf1745c7 --- /dev/null +++ b/firestore/src/include/firebase/firestore/aggregate_query_snapshot.h @@ -0,0 +1,155 @@ +/* + * Copyright 2023 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. + */ + +#ifndef FIREBASE_FIRESTORE_SRC_INCLUDE_FIREBASE_FIRESTORE_AGGREGATE_QUERY_SNAPSHOT_H_ +#define FIREBASE_FIRESTORE_SRC_INCLUDE_FIREBASE_FIRESTORE_AGGREGATE_QUERY_SNAPSHOT_H_ + +#include +#include + +namespace firebase { +namespace firestore { + +class AggregateQuery; +class AggregateQuerySnapshotInternal; + +/** + * @brief The results of executing an AggregateQuery. + * + * @note Firestore classes are not meant to be subclassed except for use in test + * mocks. Subclassing is not supported in production code and new SDK releases + * may break code that does so. + */ +class AggregateQuerySnapshot { + public: + /** + * @brief Creates an invalid AggregateQuerySnapshot that has to be reassigned + * before it can be used. + * + * Calling any member function on an invalid AggregateQuerySnapshot will be a + * no-op. If the function returns a value, it will return a zero, empty, or + * invalid value, depending on the type of the value. + */ + AggregateQuerySnapshot(); + + /** + * @brief Copy constructor. + * + * `AggregateQuerySnapshot` is immutable and can be efficiently copied (no + * deep copy is performed). + * + * @param[in] other `AggregateQuerySnapshot` to copy from. + */ + AggregateQuerySnapshot(const AggregateQuerySnapshot& other); + + /** + * @brief Move constructor. + * + * Moving is more efficient than copying for a `AggregateQuerySnapshot`. After + * being moved from, a `AggregateQuerySnapshot` is equivalent to its + * default-constructed state. + * + * @param[in] other `AggregateQuerySnapshot` to move data from. + */ + AggregateQuerySnapshot(AggregateQuerySnapshot&& other); + + virtual ~AggregateQuerySnapshot(); + + /** + * @brief Copy assignment operator. + * + * `AggregateQuerySnapshot` is immutable and can be efficiently copied (no + * deep copy is performed). + * + * @param[in] other `AggregateQuerySnapshot` to copy from. + * + * @return Reference to the destination `AggregateQuerySnapshot`. + */ + AggregateQuerySnapshot& operator=(const AggregateQuerySnapshot& other); + + /** + * @brief Move assignment operator. + * + * Moving is more efficient than copying for a `AggregateQuerySnapshot`. After + * being moved from, a `AggregateQuerySnapshot` is equivalent to its + * default-constructed state. + * + * @param[in] other `AggregateQuerySnapshot` to move data from. + * + * @return Reference to the destination `AggregateQuerySnapshot`. + */ + AggregateQuerySnapshot& operator=(AggregateQuerySnapshot&& other); + + /** + * @brief Returns the query that was executed to produce this result. + * + * @return The `AggregateQuery` instance. + */ + virtual AggregateQuery query() const; + + /** + * @brief Returns the number of documents in the result set of the underlying + * query. + * + * @return The number of documents in the result set of the underlying query. + */ + virtual int64_t count() const; + + /** + * @brief Returns true if this `AggregateQuerySnapshot` is valid, false if it + * is not valid. An invalid `AggregateQuerySnapshot` could be the result of: + * - Creating a `AggregateQuerySnapshot` using the default constructor. + * - Moving from the `AggregateQuerySnapshot`. + * - Deleting your Firestore instance, which will invalidate all the + * `AggregateQuerySnapshot` instances associated with it. + * + * @return true if this `AggregateQuerySnapshot` is valid, false if this + * `AggregateQuerySnapshot` is invalid. + */ + bool is_valid() const { return internal_ != nullptr; } + + private: + std::size_t Hash() const; + + friend bool operator==(const AggregateQuerySnapshot& lhs, + const AggregateQuerySnapshot& rhs); + friend std::size_t AggregateQuerySnapshotHash( + const AggregateQuerySnapshot& snapshot); + friend struct ConverterImpl; + friend class AggregateQuerySnapshotTest; + + template + friend struct CleanupFn; + + explicit AggregateQuerySnapshot(AggregateQuerySnapshotInternal* internal); + + mutable AggregateQuerySnapshotInternal* internal_ = nullptr; +}; + +/** Checks `lhs` and `rhs` for equality. */ +bool operator==(const AggregateQuerySnapshot& lhs, + const AggregateQuerySnapshot& rhs); + +/** Checks `lhs` and `rhs` for inequality. */ +inline bool operator!=(const AggregateQuerySnapshot& lhs, + const AggregateQuerySnapshot& rhs) { + return !(lhs == rhs); +} + +} // namespace firestore +} // namespace firebase + +#endif // FIREBASE_FIRESTORE_SRC_INCLUDE_FIREBASE_FIRESTORE_AGGREGATE_QUERY_SNAPSHOT_H_ diff --git a/firestore/src/include/firebase/firestore/aggregate_source.h b/firestore/src/include/firebase/firestore/aggregate_source.h new file mode 100644 index 0000000000..335d9f6dce --- /dev/null +++ b/firestore/src/include/firebase/firestore/aggregate_source.h @@ -0,0 +1,46 @@ +/* + * Copyright 2023 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. + */ + +#ifndef FIREBASE_FIRESTORE_SRC_INCLUDE_FIREBASE_FIRESTORE_AGGREGATE_SOURCE_H_ +#define FIREBASE_FIRESTORE_SRC_INCLUDE_FIREBASE_FIRESTORE_AGGREGATE_SOURCE_H_ + +namespace firebase { +namespace firestore { + +/** + * @brief The sources from which AggregateQuery::Get can retrieve its + * results. + */ +enum class AggregateSource { + /** + * Perform the aggregation on the server and download the result. + * + * The result received from the server is presented, unaltered, without + * considering any local state. That is, documents in the local cache are not + * taken into consideration, neither are local modifications not yet + * synchronized with the server. Previously-downloaded results, if any, are + * not used: every request using this source necessarily involves a round trip + * to the server. + * + * The AggregateQuery will fail if the server cannot be reached, such as if + * the client is offline. + */ + kServer, +}; + +} // namespace firestore +} // namespace firebase +#endif // FIREBASE_FIRESTORE_SRC_INCLUDE_FIREBASE_FIRESTORE_AGGREGATE_SOURCE_H_ diff --git a/firestore/src/include/firebase/firestore/query.h b/firestore/src/include/firebase/firestore/query.h index d2e73400ce..0586756854 100644 --- a/firestore/src/include/firebase/firestore/query.h +++ b/firestore/src/include/firebase/firestore/query.h @@ -37,6 +37,7 @@ class Future; namespace firestore { +class AggregateQuery; class DocumentSnapshot; template class EventListener; @@ -143,6 +144,23 @@ class Query { */ virtual Firestore* firestore(); + /** + * @brief Returns a query that counts the documents in the result set of this + * query. + * + * The returned query, when executed, counts the documents in the result set + * of this query without actually downloading the documents. + * + * Using the returned query to count the documents is efficient because only + * the final count, not the documents' data, is downloaded. The returned query + * can even count the documents if the result set would be prohibitively large + * to download entirely (e.g. thousands of documents). + * + * @return An aggregate query that counts the documents in the result set of + * this query. + */ + virtual AggregateQuery Count() const; + /** * @brief Creates and returns a new Query with the additional filter that * documents must contain the specified field and the value should be equal to diff --git a/firestore/src/main/aggregate_query_main.cc b/firestore/src/main/aggregate_query_main.cc new file mode 100644 index 0000000000..2b1a0aa2ee --- /dev/null +++ b/firestore/src/main/aggregate_query_main.cc @@ -0,0 +1,80 @@ +/* + * Copyright 2023 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. + */ + +#include + +#include "firestore/src/main/aggregate_query_main.h" + +#include "Firestore/core/src/api/aggregate_query.h" +#include "Firestore/core/src/api/query_core.h" +#include "firestore/src/include/firebase/firestore/aggregate_query.h" +#include "firestore/src/include/firebase/firestore/aggregate_query_snapshot.h" +#include "firestore/src/main/aggregate_query_snapshot_main.h" +#include "firestore/src/main/listener_main.h" +#include "firestore/src/main/promise_factory_main.h" +#include "firestore/src/main/util_main.h" + +namespace firebase { +namespace firestore { + +AggregateQueryInternal::AggregateQueryInternal( + api::AggregateQuery&& aggregate_query) + : aggregate_query_{std::move(aggregate_query)}, + promise_factory_{PromiseFactory::Create(this)} {} + +FirestoreInternal* AggregateQueryInternal::firestore_internal() { + return GetFirestoreInternal(&aggregate_query_.query()); +} + +const FirestoreInternal* AggregateQueryInternal::firestore_internal() const { + return GetFirestoreInternal(&aggregate_query_.query()); +} + +Query AggregateQueryInternal::query() { + return MakePublic(api::Query(aggregate_query_.query())); +} + +Future AggregateQueryInternal::Get( + AggregateSource source) { + api::AggregateQuery aggregate_query = aggregate_query_; + auto promise = + promise_factory_.CreatePromise(AsyncApis::kGet); + // TODO(C++14) + // https://en.wikipedia.org/wiki/C%2B%2B14#Lambda_capture_expressions + aggregate_query_.Get( + [aggregate_query, promise](util::StatusOr maybe_value) mutable { + if (maybe_value.ok()) { + int64_t count = maybe_value.ValueOrDie(); + AggregateQuerySnapshotInternal internal{std::move(aggregate_query), + count}; + AggregateQuerySnapshot snapshot = MakePublic(std::move(internal)); + promise.SetValue(std::move(snapshot)); + } else { + promise.SetError(maybe_value.status()); + } + }); + return promise.future(); +} + +bool operator==(const AggregateQueryInternal& lhs, + const AggregateQueryInternal& rhs) { + // TODO(b/276440573) - there needs to be equals operator defined on + // api::AggregateQuery + return lhs.aggregate_query_.query() == rhs.aggregate_query_.query(); +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/main/aggregate_query_main.h b/firestore/src/main/aggregate_query_main.h new file mode 100644 index 0000000000..3fb47cb095 --- /dev/null +++ b/firestore/src/main/aggregate_query_main.h @@ -0,0 +1,69 @@ +/* + * Copyright 2023 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. + */ + +#ifndef FIREBASE_FIRESTORE_SRC_MAIN_AGGREGATE_QUERY_MAIN_H_ +#define FIREBASE_FIRESTORE_SRC_MAIN_AGGREGATE_QUERY_MAIN_H_ + +#include "Firestore/core/src/api/aggregate_query.h" +#include "firestore/src/include/firebase/firestore/aggregate_source.h" +#include "firestore/src/main/firestore_main.h" + +#if defined(__ANDROID__) +#error "This header should not be used on Android." +#endif + +namespace firebase { +namespace firestore { + +class AggregateQuerySnapshot; + +class AggregateQueryInternal { + public: + explicit AggregateQueryInternal(api::AggregateQuery&& aggregate_query); + + FirestoreInternal* firestore_internal(); + const FirestoreInternal* firestore_internal() const; + + Query query(); + + Future Get(AggregateSource source); + + size_t Hash() const { return aggregate_query_.query().Hash(); } + + friend bool operator==(const AggregateQueryInternal& lhs, + const AggregateQueryInternal& rhs); + + private: + enum class AsyncApis { + kGet, + kCount, + }; + + friend class AggregateQuerySnapshotTest; + + api::AggregateQuery aggregate_query_; + PromiseFactory promise_factory_; +}; + +inline bool operator!=(const AggregateQueryInternal& lhs, + const AggregateQueryInternal& rhs) { + return !(lhs == rhs); +} + +} // namespace firestore +} // namespace firebase + +#endif // FIREBASE_FIRESTORE_SRC_MAIN_AGGREGATE_QUERY_MAIN_H_ diff --git a/firestore/src/main/aggregate_query_snapshot_main.cc b/firestore/src/main/aggregate_query_snapshot_main.cc new file mode 100644 index 0000000000..4565b409e1 --- /dev/null +++ b/firestore/src/main/aggregate_query_snapshot_main.cc @@ -0,0 +1,55 @@ +/* + * Copyright 2023 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. + */ + +#include "firestore/src/main/aggregate_query_snapshot_main.h" + +#include +#include "firestore/src/main/converter_main.h" +#include "firestore/src/main/util_main.h" + +namespace firebase { +namespace firestore { + +AggregateQuerySnapshotInternal::AggregateQuerySnapshotInternal( + api::AggregateQuery&& aggregate_query, int64_t count_result) + : aggregate_query_(std::move(aggregate_query)), + count_result_(count_result) {} + +FirestoreInternal* AggregateQuerySnapshotInternal::firestore_internal() { + return GetFirestoreInternal(&aggregate_query_.query()); +} + +const FirestoreInternal* AggregateQuerySnapshotInternal::firestore_internal() + const { + return GetFirestoreInternal(&aggregate_query_.query()); +} + +AggregateQuery AggregateQuerySnapshotInternal::query() const { + return MakePublic(api::AggregateQuery(aggregate_query_)); +} + +int64_t AggregateQuerySnapshotInternal::count() const { return count_result_; } + +bool operator==(const AggregateQuerySnapshotInternal& lhs, + const AggregateQuerySnapshotInternal& rhs) { + // TODO(b/276440573) - there needs to be equals operator defined on + // api::AggregateQuery + return lhs.aggregate_query_.query() == rhs.aggregate_query_.query() && + lhs.count_result_ == rhs.count_result_; +} + +} // namespace firestore +} // namespace firebase diff --git a/firestore/src/main/aggregate_query_snapshot_main.h b/firestore/src/main/aggregate_query_snapshot_main.h new file mode 100644 index 0000000000..222c6b4e39 --- /dev/null +++ b/firestore/src/main/aggregate_query_snapshot_main.h @@ -0,0 +1,65 @@ +/* + * Copyright 2023 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. + */ + +#ifndef FIREBASE_FIRESTORE_SRC_MAIN_AGGREGATE_QUERY_SNAPSHOT_MAIN_H_ +#define FIREBASE_FIRESTORE_SRC_MAIN_AGGREGATE_QUERY_SNAPSHOT_MAIN_H_ + +#include +#include + +#include "Firestore/core/src/api/aggregate_query.h" +#include "firestore/src/include/firebase/firestore/aggregate_query.h" +#include "firestore/src/main/firestore_main.h" + +#if defined(__ANDROID__) +#error "This header should not be used on Android." +#endif + +namespace firebase { +namespace firestore { + +class AggregateQuerySnapshotInternal { + public: + explicit AggregateQuerySnapshotInternal(api::AggregateQuery&& aggregate_query, + int64_t count); + + FirestoreInternal* firestore_internal(); + const FirestoreInternal* firestore_internal() const; + + AggregateQuery query() const; + int64_t count() const; + + std::size_t Hash() const { + return util::Hash(aggregate_query_.query().Hash(), count_result_); + } + + friend bool operator==(const AggregateQuerySnapshotInternal& lhs, + const AggregateQuerySnapshotInternal& rhs); + + private: + api::AggregateQuery aggregate_query_; + int64_t count_result_ = 0; +}; + +inline bool operator!=(const AggregateQuerySnapshotInternal& lhs, + const AggregateQuerySnapshotInternal& rhs) { + return !(lhs == rhs); +} + +} // namespace firestore +} // namespace firebase + +#endif // FIREBASE_FIRESTORE_SRC_MAIN_AGGREGATE_QUERY_SNAPSHOT_MAIN_H_ diff --git a/firestore/src/main/converter_main.h b/firestore/src/main/converter_main.h index 38dd62fb85..14dac38a59 100644 --- a/firestore/src/main/converter_main.h +++ b/firestore/src/main/converter_main.h @@ -20,6 +20,7 @@ #include #include +#include "Firestore/core/src/api/aggregate_query.h" #include "Firestore/core/src/api/collection_reference.h" #include "Firestore/core/src/api/document_change.h" #include "Firestore/core/src/api/document_reference.h" @@ -30,8 +31,12 @@ #include "Firestore/core/src/core/transaction.h" #include "Firestore/core/src/model/field_path.h" #include "absl/memory/memory.h" +#include "firebase/firestore/aggregate_query.h" +#include "firebase/firestore/aggregate_query_snapshot.h" #include "firestore/src/common/type_mapping.h" #include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/main/aggregate_query_main.h" +#include "firestore/src/main/aggregate_query_snapshot_main.h" #include "firestore/src/main/collection_reference_main.h" #include "firestore/src/main/document_change_main.h" #include "firestore/src/main/document_reference_main.h" @@ -82,6 +87,10 @@ struct ConverterImpl { // MakePublic +inline AggregateQuery MakePublic(api::AggregateQuery&& from) { + return ConverterImpl::MakePublicFromCore(std::move(from)); +} + inline CollectionReference MakePublic(api::CollectionReference&& from) { return ConverterImpl::MakePublicFromCore( std::move(from)); @@ -103,6 +112,12 @@ inline FieldValue MakePublic(FieldValueInternal&& from) { return ConverterImpl::MakePublicFromInternal(std::move(from)); } +inline AggregateQuerySnapshot MakePublic( + AggregateQuerySnapshotInternal&& from) { + return ConverterImpl::MakePublicFromInternal( + std::move(from)); +} + inline ListenerRegistration MakePublic( std::unique_ptr from, FirestoreInternal* firestore) { diff --git a/firestore/src/main/query_main.cc b/firestore/src/main/query_main.cc index 070e113fc1..ffed058c9e 100644 --- a/firestore/src/main/query_main.cc +++ b/firestore/src/main/query_main.cc @@ -18,6 +18,7 @@ #include +#include "Firestore/core/src/api/aggregate_query.h" #include "Firestore/core/src/api/listener_registration.h" #include "Firestore/core/src/core/filter.h" #include "Firestore/core/src/core/listen_options.h" @@ -33,6 +34,7 @@ #include "firestore/src/common/hard_assert_common.h" #include "firestore/src/common/macros.h" #include "firestore/src/include/firebase/firestore.h" +#include "firestore/src/main/aggregate_query_main.h" #include "firestore/src/main/converter_main.h" #include "firestore/src/main/document_snapshot_main.h" #include "firestore/src/main/listener_main.h" @@ -94,6 +96,8 @@ Future QueryInternal::Get(Source source) { return promise.future(); } +AggregateQuery QueryInternal::Count() { return MakePublic(query_.Count()); } + Query QueryInternal::Where(const FieldPath& field_path, Operator op, const FieldValue& value) const { diff --git a/firestore/src/main/query_main.h b/firestore/src/main/query_main.h index 4f3e89016d..958df81f03 100644 --- a/firestore/src/main/query_main.h +++ b/firestore/src/main/query_main.h @@ -57,6 +57,8 @@ class QueryInternal { virtual Future Get(Source source); + AggregateQuery Count(); + ListenerRegistration AddSnapshotListener( MetadataChanges metadata_changes, EventListener* listener); diff --git a/release_build_files/readme.md b/release_build_files/readme.md index 6eeb9b6e42..a3ecb045c3 100644 --- a/release_build_files/readme.md +++ b/release_build_files/readme.md @@ -642,6 +642,11 @@ workflow use only during the development of your app, not for publicly shipping code. ## Release Notes +### Upcoming Release +- Changes + - Firestore: Added `Query::Count()`, which fetches the number of documents in the result + set without actually downloading the documents ([#1207](https://github.com/firebase/firebase-cpp-sdk/pull/1207)). + ### 10.7.0 - Changes - General (Android): Update to Firebase Android BoM version 31.3.0.