From 215b366c8ff705d21019197c547b4c1ce28c158e Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 23 Mar 2023 12:55:51 -0400 Subject: [PATCH 1/4] AggregateQuery count implementation and tests --- firestore/src/AggregateQuery.cs | 0 firestore/src/AggregateQuerySnapshot.cs | 240 ++++++++++++++++++++++++ firestore/src/AggregateSource.cs | 36 ++++ 3 files changed, 276 insertions(+) create mode 100644 firestore/src/AggregateQuery.cs create mode 100644 firestore/src/AggregateQuerySnapshot.cs create mode 100644 firestore/src/AggregateSource.cs diff --git a/firestore/src/AggregateQuery.cs b/firestore/src/AggregateQuery.cs new file mode 100644 index 00000000..e69de29b diff --git a/firestore/src/AggregateQuerySnapshot.cs b/firestore/src/AggregateQuerySnapshot.cs new file mode 100644 index 00000000..65b1b29f --- /dev/null +++ b/firestore/src/AggregateQuerySnapshot.cs @@ -0,0 +1,240 @@ +// Copyright 2017 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using BclType = System.Type; +using Firebase.Firestore.Internal; + +namespace Firebase.Firestore { + /// + /// An immutable snapshot of the data for a document. + /// + /// + /// A DocumentSnapshot contains data read from a document in your Cloud Firestore + /// database. The data can be extracted with the + /// + /// or methods. + /// + /// + /// If the DocumentSnapshot points to a non-existing document, ToDictionary + /// will return null. You can always explicitly check for a document's existence by + /// checking . + /// + public sealed class DocumentSnapshot { + private readonly DocumentSnapshotProxy _proxy; + private readonly FirebaseFirestore _firestore; + + internal DocumentSnapshotProxy Proxy { + get { + return _proxy; + } + } + + internal DocumentSnapshot(DocumentSnapshotProxy proxy, FirebaseFirestore firestore) { + _proxy = Util.NotNull(proxy); + _firestore = Util.NotNull(firestore); + } + + /// + /// The full reference to the document. + /// + public DocumentReference Reference => new DocumentReference(_proxy.reference(), _firestore); + + /// + /// The ID of the document. + /// + public string Id => _proxy.id(); + + /// + /// Whether or not the document exists. + /// + public bool Exists => _proxy.exists(); + + /// + /// The metadata for this DocumentSnapshot. + /// + public SnapshotMetadata Metadata { + get { + return SnapshotMetadata.ConvertFromProxy(_proxy.metadata()); + } + } + + /// + /// Returns the document data as a . + /// + /// Configures the behavior for server timestamps that + /// have not yet been set to their final value. + /// A containing the document data or + /// null if this is a nonexistent document. + public Dictionary ToDictionary(ServerTimestampBehavior serverTimestampBehavior = ServerTimestampBehavior.None) + => ConvertTo>(serverTimestampBehavior); + + /// + /// Deserializes the document data as the specified type. + /// + /// The type to deserialize the document data as. + /// Configures the behavior for server timestamps that + /// have not yet been set to their final value. + /// The deserialized data or default(T) if this is a nonexistent document. + /// + public T ConvertTo(ServerTimestampBehavior serverTimestampBehavior = ServerTimestampBehavior.None) { + // C++ returns a non-existent document as an empty map, because the map is returned by value. + // For reference types, it makes more sense to return a non-existent document as a null + // value. This matches Android behavior. + var targetType = typeof(T); + if (!Exists) { + BclType underlyingType = Nullable.GetUnderlyingType(targetType); + if (!targetType.IsValueType || underlyingType != null) { + return default(T); + } else { + throw new ArgumentException(String.Format( + "Unable to convert a non-existent document to {0}", targetType.FullName)); + } + } + + var map = FirestoreCpp.ConvertSnapshotToFieldValue(_proxy, serverTimestampBehavior.ConvertToProxy()); + var context = new DeserializationContext(this); + return (T)ValueDeserializer.Deserialize(context, map, targetType); + } + + /// + /// Fetches a field value from the document, throwing an exception if the field does not exist. + /// + /// The dot-separated field path to fetch. Must not be null or + /// empty. + /// Configures the behavior for server timestamps that + /// have not yet been set to their final value. + /// The field does not exist in the document data. + /// + /// The deserialized value. + public T GetValue(string path, ServerTimestampBehavior serverTimestampBehavior = + ServerTimestampBehavior.None) { + Preconditions.CheckNotNullOrEmpty(path, nameof(path)); + return GetValue(FieldPath.FromDotSeparatedString(path), serverTimestampBehavior); + } + + /// + /// Attempts to fetch the given field value from the document, returning whether or not it was + /// found. + /// + /// + /// This method does not throw an exception if the field is not found, but does throw an + /// exception if the field was found but cannot be deserialized. + /// + /// The dot-separated field path to fetch. Must not be null or empty. + /// + /// When this method returns, contains the deserialized value if the field + /// was found, or the default value of otherwise. + /// Configures the behavior for server timestamps that + /// have not yet been set to their final value. + /// true if the field was found in the document; false otherwise. + /// + public bool TryGetValue( + string path, out T value, + ServerTimestampBehavior serverTimestampBehavior = ServerTimestampBehavior.None) { + Preconditions.CheckNotNullOrEmpty(path, nameof(path)); + return TryGetValue(FieldPath.FromDotSeparatedString(path), out value, + serverTimestampBehavior); + } + + /// + /// Fetches a field value from the document, throwing an exception if the field does not exist. + /// + /// The field path to fetch. Must not be null or empty. + /// Configures the behavior for server timestamps that + /// have not yet been set to their final value. + /// The field does not exist in the document data. + /// + /// The deserialized value. + public T GetValue(FieldPath path, ServerTimestampBehavior serverTimestampBehavior = ServerTimestampBehavior.None) { + Preconditions.CheckNotNull(path, nameof(path)); + T value; + if (TryGetValue(path, out value, serverTimestampBehavior)) { + return value; + } else { + throw new InvalidOperationException("Field " + path + " not found in document"); + } + } + + /// + /// Attempts to fetch the given field value from the document, returning whether or not it was + /// found. + /// + /// + /// This method does not throw an exception if the field is not found, but does throw an + /// exception if the field was found but cannot be deserialized. + /// + /// The field path to fetch. Must not be null or empty. + /// When this method returns, contains the deserialized value if the field + /// was found, or the default value of otherwise. + /// Configures the behavior for server timestamps that + /// have not yet been set to their final value. + /// true if the field was found in the document; false otherwise. + /// + public bool TryGetValue(FieldPath path, out T value, ServerTimestampBehavior serverTimestampBehavior = ServerTimestampBehavior.None) { + Preconditions.CheckNotNull(path, nameof(path)); + value = default(T); + if (!Exists) { + return false; + } else { + FieldValueProxy fvi = _proxy.Get(path.ConvertToProxy(), serverTimestampBehavior.ConvertToProxy()); + if (!fvi.is_valid()) { + return false; + } else { + var context = new DeserializationContext(this); + value = (T)ValueDeserializer.Deserialize(context, fvi, typeof(T)); + return true; + } + } + } + + /// + /// Determines whether or not the given field path is present in the document. If this snapshot + /// represents a missing document, this method will always return false. + /// + /// The dot-separated field path to check. Must not be null or empty. + /// + /// true if the specified path represents a field in the document; false + /// otherwise. + public bool ContainsField(string path) { + Preconditions.CheckNotNullOrEmpty(path, nameof(path)); + return ContainsField(FieldPath.FromDotSeparatedString(path)); + } + + /// + /// Determines whether or not the given field path is present in the document. If this snapshot + /// represents a missing document, this method will always return false. + /// + /// The field path to check. Must not be null. + /// true if the specified path represents a field in the document; false + /// otherwise. + public bool ContainsField(FieldPath path) { + Preconditions.CheckNotNull(path, nameof(path)); + return Exists && _proxy.Get(path.ConvertToProxy()).is_valid(); + } + + /// + public override bool Equals(object obj) => Equals(obj as DocumentSnapshot); + + /// + public bool Equals(DocumentSnapshot other) => + other != null && FirestoreCpp.DocumentSnapshotEquals(_proxy, other._proxy); + + /// + public override int GetHashCode() { + return FirestoreCpp.DocumentSnapshotHashCode(_proxy); + } + } +} diff --git a/firestore/src/AggregateSource.cs b/firestore/src/AggregateSource.cs new file mode 100644 index 00000000..8fe670c4 --- /dev/null +++ b/firestore/src/AggregateSource.cs @@ -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 +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Firebase.Firestore { + + /// + /// The sources from which an can retrieve its + /// results. + /// + + public enum 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. + Server, +} + +} From 4212b66f6a6a332bf7bffd5d5c777fa706260fdf Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 23 Mar 2023 13:04:25 -0400 Subject: [PATCH 2/4] Implement AggregateQuery Count with tests --- firestore/CMakeLists.txt | 3 + firestore/src/AggregateQuery.cs | 36 +++ firestore/src/AggregateQuerySnapshot.cs | 215 ++---------------- firestore/src/Query.cs | 21 ++ firestore/src/internal/Enums.cs | 9 + firestore/src/swig/equality_compare.cc | 8 + firestore/src/swig/equality_compare.h | 12 +- firestore/src/swig/firestore.i | 19 ++ firestore/src/swig/hash.cc | 20 ++ firestore/src/swig/hash.h | 8 + .../Sample/Firestore/UIHandlerAutomated.cs | 83 +++++++ 11 files changed, 234 insertions(+), 200 deletions(-) diff --git a/firestore/CMakeLists.txt b/firestore/CMakeLists.txt index 85c8739c..d6c484dc 100644 --- a/firestore/CMakeLists.txt +++ b/firestore/CMakeLists.txt @@ -23,6 +23,9 @@ set(firebase_firestore_swig # Firebase Firestore CSharp files that should be included in reference docs set(firebase_firestore_src_documented + src/AggregateQuery.cs + src/AggregateQuerySnapshot.cs + src/AggregateSource.cs src/Blob.cs src/CollectionReference.cs src/DocumentChange.cs diff --git a/firestore/src/AggregateQuery.cs b/firestore/src/AggregateQuery.cs index e69de29b..804e3334 100644 --- a/firestore/src/AggregateQuery.cs +++ b/firestore/src/AggregateQuery.cs @@ -0,0 +1,36 @@ + + +using Firebase.Firestore.Internal; +using Firebase.Platform; +using System.Threading.Tasks; + +namespace Firebase.Firestore { + public sealed class AggregateQuery + { + private readonly AggregateQueryProxy _proxy; + private readonly FirebaseFirestore _firestore; + + internal AggregateQuery(AggregateQueryProxy proxy, FirebaseFirestore firestore) { + _proxy = Util.NotNull(proxy); + _firestore = Util.NotNull(firestore); + } + + /// + /// The query of aggregations that will be calculated. + /// + public Query Query => new Query(_proxy.query(), _firestore); + + public Task GetSnapshotAsync(AggregateSource source) { + var sourceProxy = Enums.Convert(source); + return Util.MapResult(_proxy.GetAsync(sourceProxy), + taskResult => { return new AggregateQuerySnapshot(taskResult, _firestore); }); + } + + public override bool Equals(object obj) => Equals(obj as Query); + public bool Equals(AggregateQuery other) => other != null && FirestoreCpp.AggregateQueryEquals(_proxy, other._proxy); + + public override int GetHashCode() { + return FirestoreCpp.AggregateQueryHashCode(_proxy); + } + } +} \ No newline at end of file diff --git a/firestore/src/AggregateQuerySnapshot.cs b/firestore/src/AggregateQuerySnapshot.cs index 65b1b29f..7ac7ce17 100644 --- a/firestore/src/AggregateQuerySnapshot.cs +++ b/firestore/src/AggregateQuerySnapshot.cs @@ -16,225 +16,42 @@ using System.Collections.Generic; using BclType = System.Type; using Firebase.Firestore.Internal; +using System.Threading.Tasks; namespace Firebase.Firestore { - /// - /// An immutable snapshot of the data for a document. - /// - /// - /// A DocumentSnapshot contains data read from a document in your Cloud Firestore - /// database. The data can be extracted with the - /// - /// or methods. - /// - /// - /// If the DocumentSnapshot points to a non-existing document, ToDictionary - /// will return null. You can always explicitly check for a document's existence by - /// checking . - /// - public sealed class DocumentSnapshot { - private readonly DocumentSnapshotProxy _proxy; + public sealed class AggregateQuerySnapshot { + private readonly AggregateQuerySnapshotProxy _proxy; private readonly FirebaseFirestore _firestore; - internal DocumentSnapshotProxy Proxy { - get { - return _proxy; - } - } - - internal DocumentSnapshot(DocumentSnapshotProxy proxy, FirebaseFirestore firestore) { + internal AggregateQuerySnapshot(AggregateQuerySnapshotProxy proxy, FirebaseFirestore firestore) { _proxy = Util.NotNull(proxy); _firestore = Util.NotNull(firestore); } - /// - /// The full reference to the document. + /// The aggregate query producing this snapshot. /// - public DocumentReference Reference => new DocumentReference(_proxy.reference(), _firestore); - - /// - /// The ID of the document. - /// - public string Id => _proxy.id(); - - /// - /// Whether or not the document exists. - /// - public bool Exists => _proxy.exists(); - - /// - /// The metadata for this DocumentSnapshot. - /// - public SnapshotMetadata Metadata { + public AggregateQuery Query { get { - return SnapshotMetadata.ConvertFromProxy(_proxy.metadata()); - } - } - - /// - /// Returns the document data as a . - /// - /// Configures the behavior for server timestamps that - /// have not yet been set to their final value. - /// A containing the document data or - /// null if this is a nonexistent document. - public Dictionary ToDictionary(ServerTimestampBehavior serverTimestampBehavior = ServerTimestampBehavior.None) - => ConvertTo>(serverTimestampBehavior); - - /// - /// Deserializes the document data as the specified type. - /// - /// The type to deserialize the document data as. - /// Configures the behavior for server timestamps that - /// have not yet been set to their final value. - /// The deserialized data or default(T) if this is a nonexistent document. - /// - public T ConvertTo(ServerTimestampBehavior serverTimestampBehavior = ServerTimestampBehavior.None) { - // C++ returns a non-existent document as an empty map, because the map is returned by value. - // For reference types, it makes more sense to return a non-existent document as a null - // value. This matches Android behavior. - var targetType = typeof(T); - if (!Exists) { - BclType underlyingType = Nullable.GetUnderlyingType(targetType); - if (!targetType.IsValueType || underlyingType != null) { - return default(T); - } else { - throw new ArgumentException(String.Format( - "Unable to convert a non-existent document to {0}", targetType.FullName)); - } + return new AggregateQuery(_proxy.query(), _firestore); } - - var map = FirestoreCpp.ConvertSnapshotToFieldValue(_proxy, serverTimestampBehavior.ConvertToProxy()); - var context = new DeserializationContext(this); - return (T)ValueDeserializer.Deserialize(context, map, targetType); } - /// - /// Fetches a field value from the document, throwing an exception if the field does not exist. - /// - /// The dot-separated field path to fetch. Must not be null or - /// empty. - /// Configures the behavior for server timestamps that - /// have not yet been set to their final value. - /// The field does not exist in the document data. - /// - /// The deserialized value. - public T GetValue(string path, ServerTimestampBehavior serverTimestampBehavior = - ServerTimestampBehavior.None) { - Preconditions.CheckNotNullOrEmpty(path, nameof(path)); - return GetValue(FieldPath.FromDotSeparatedString(path), serverTimestampBehavior); - } - - /// - /// Attempts to fetch the given field value from the document, returning whether or not it was - /// found. - /// - /// - /// This method does not throw an exception if the field is not found, but does throw an - /// exception if the field was found but cannot be deserialized. - /// - /// The dot-separated field path to fetch. Must not be null or empty. - /// - /// When this method returns, contains the deserialized value if the field - /// was found, or the default value of otherwise. - /// Configures the behavior for server timestamps that - /// have not yet been set to their final value. - /// true if the field was found in the document; false otherwise. - /// - public bool TryGetValue( - string path, out T value, - ServerTimestampBehavior serverTimestampBehavior = ServerTimestampBehavior.None) { - Preconditions.CheckNotNullOrEmpty(path, nameof(path)); - return TryGetValue(FieldPath.FromDotSeparatedString(path), out value, - serverTimestampBehavior); - } - - /// - /// Fetches a field value from the document, throwing an exception if the field does not exist. - /// - /// The field path to fetch. Must not be null or empty. - /// Configures the behavior for server timestamps that - /// have not yet been set to their final value. - /// The field does not exist in the document data. - /// - /// The deserialized value. - public T GetValue(FieldPath path, ServerTimestampBehavior serverTimestampBehavior = ServerTimestampBehavior.None) { - Preconditions.CheckNotNull(path, nameof(path)); - T value; - if (TryGetValue(path, out value, serverTimestampBehavior)) { - return value; - } else { - throw new InvalidOperationException("Field " + path + " not found in document"); - } - } - - /// - /// Attempts to fetch the given field value from the document, returning whether or not it was - /// found. - /// - /// - /// This method does not throw an exception if the field is not found, but does throw an - /// exception if the field was found but cannot be deserialized. - /// - /// The field path to fetch. Must not be null or empty. - /// When this method returns, contains the deserialized value if the field - /// was found, or the default value of otherwise. - /// Configures the behavior for server timestamps that - /// have not yet been set to their final value. - /// true if the field was found in the document; false otherwise. - /// - public bool TryGetValue(FieldPath path, out T value, ServerTimestampBehavior serverTimestampBehavior = ServerTimestampBehavior.None) { - Preconditions.CheckNotNull(path, nameof(path)); - value = default(T); - if (!Exists) { - return false; - } else { - FieldValueProxy fvi = _proxy.Get(path.ConvertToProxy(), serverTimestampBehavior.ConvertToProxy()); - if (!fvi.is_valid()) { - return false; - } else { - var context = new DeserializationContext(this); - value = (T)ValueDeserializer.Deserialize(context, fvi, typeof(T)); - return true; - } + public long Count { + get { + return _proxy.count(); } } - - /// - /// Determines whether or not the given field path is present in the document. If this snapshot - /// represents a missing document, this method will always return false. - /// - /// The dot-separated field path to check. Must not be null or empty. - /// - /// true if the specified path represents a field in the document; false - /// otherwise. - public bool ContainsField(string path) { - Preconditions.CheckNotNullOrEmpty(path, nameof(path)); - return ContainsField(FieldPath.FromDotSeparatedString(path)); - } - - /// - /// Determines whether or not the given field path is present in the document. If this snapshot - /// represents a missing document, this method will always return false. - /// - /// The field path to check. Must not be null. - /// true if the specified path represents a field in the document; false - /// otherwise. - public bool ContainsField(FieldPath path) { - Preconditions.CheckNotNull(path, nameof(path)); - return Exists && _proxy.Get(path.ConvertToProxy()).is_valid(); - } - + /// - public override bool Equals(object obj) => Equals(obj as DocumentSnapshot); + public override bool Equals(object obj) => Equals(obj as AggregateQuerySnapshot); /// - public bool Equals(DocumentSnapshot other) => - other != null && FirestoreCpp.DocumentSnapshotEquals(_proxy, other._proxy); - + public bool Equals(AggregateQuerySnapshot other) => + other != null && FirestoreCpp.AggregateQuerySnapshotEquals(_proxy, other._proxy); + /// public override int GetHashCode() { - return FirestoreCpp.DocumentSnapshotHashCode(_proxy); + return FirestoreCpp.AggregateQuerySnapshotHashCode(_proxy); } } } diff --git a/firestore/src/Query.cs b/firestore/src/Query.cs index b9628a3d..3bd60fe9 100644 --- a/firestore/src/Query.cs +++ b/firestore/src/Query.cs @@ -53,6 +53,27 @@ public FirebaseFirestore Firestore { } } + /// + /// 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). + /// + /// + /// An aggregate query that counts the documents in the result set of this query. + /// + public AggregateQuery Count { + get { + return new AggregateQuery(_proxy.Count(), _firestore); + } + } + + /// /// Creates and returns a new Query with the additional filter that documents must /// contain the specified field and the value should be equal to the specified value. diff --git a/firestore/src/internal/Enums.cs b/firestore/src/internal/Enums.cs index 20322f76..2738c259 100644 --- a/firestore/src/internal/Enums.cs +++ b/firestore/src/internal/Enums.cs @@ -19,6 +19,15 @@ namespace Firebase.Firestore.Internal { /// documentation purposes -- they are otherwise equivalent to the proxy ones. static class Enums { + public static AggregateSourceProxy Convert(AggregateSource aggregateSource) { + switch (aggregateSource) { + case AggregateSource.Server: + return AggregateSourceProxy.Server; + default: + throw new System.ArgumentOutOfRangeException("Unexpected enum value: " + + aggregateSource.ToString()); + } + } public static SourceProxy Convert(Source source) { switch (source) { case Source.Default: diff --git a/firestore/src/swig/equality_compare.cc b/firestore/src/swig/equality_compare.cc index da739f39..63f4098a 100644 --- a/firestore/src/swig/equality_compare.cc +++ b/firestore/src/swig/equality_compare.cc @@ -13,6 +13,14 @@ namespace firebase { namespace firestore { namespace csharp { +bool AggregateQueryEquals(const AggregateQuery* lhs, const AggregateQuery* rhs) { + return EqualityCompareHelper(lhs, rhs); +} + +bool AggregateQuerySnapshotEquals(const AggregateQuerySnapshot* lhs, const AggregateQuerySnapshot* rhs) { + return EqualityCompareHelper(lhs, rhs); +} + bool QueryEquals(const Query* lhs, const Query* rhs) { return EqualityCompareHelper(lhs, rhs); } diff --git a/firestore/src/swig/equality_compare.h b/firestore/src/swig/equality_compare.h index 178c8131..a35d05f8 100644 --- a/firestore/src/swig/equality_compare.h +++ b/firestore/src/swig/equality_compare.h @@ -1,6 +1,8 @@ #ifndef FIREBASE_FIRESTORE_CLIENT_UNITY_SRC_SWIG_EQUALITY_COMPARE_H_ #define FIREBASE_FIRESTORE_CLIENT_UNITY_SRC_SWIG_EQUALITY_COMPARE_H_ +#include "firestore/src/include/firebase/firestore/aggregate_query.h" +#include "firestore/src/include/firebase/firestore/aggregate_query_snapshot.h" #include "firestore/src/include/firebase/firestore/document_change.h" #include "firestore/src/include/firebase/firestore/document_snapshot.h" #include "firestore/src/include/firebase/firestore/query.h" @@ -10,7 +12,15 @@ namespace firebase { namespace firestore { namespace csharp { -// Compares two `Query` objects for equality using their `==` operator, handling +// Compares two `AggregateQuery` objects for equality using their `==` operator, handling +// one or both of them being `nullptr`. +bool AggregateQueryEquals(const AggregateQuery* lhs, const AggregateQuery* rhs); + +// Compares two `AggregateQuerySnapshot` objects for equality using their `==` operator, +// handling one or both of them being `nullptr`. +bool AggregateQuerySnapshotEquals(const AggregateQuerySnapshot* lhs, const AggregateQuerySnapshot* rhs); + + // Compares two `Query` objects for equality using their `==` operator, handling // one or both of them being `nullptr`. bool QueryEquals(const Query* lhs, const Query* rhs); diff --git a/firestore/src/swig/firestore.i b/firestore/src/swig/firestore.i index e1350295..b976faa3 100644 --- a/firestore/src/swig/firestore.i +++ b/firestore/src/swig/firestore.i @@ -201,6 +201,8 @@ SWIG_MAP_CFUNC_TO_CSDELEGATE(::firebase::firestore::csharp::LoadBundleTaskProgre // Generate Future instantiations, must be before other wrappers. %include "app/src/swig/future.i" +%SWIG_FUTURE(Future_AggregateQuerySnapshot, AggregateQuerySnapshotProxy, internal, + firebase::firestore::AggregateQuerySnapshot, FirestoreException) %SWIG_FUTURE(Future_QuerySnapshot, QuerySnapshotProxy, internal, firebase::firestore::QuerySnapshot, FirestoreException) %SWIG_FUTURE(Future_DocumentSnapshot, DocumentSnapshotProxy, internal, @@ -250,6 +252,10 @@ SWIG_CREATE_PROXY(firebase::firestore::ListenerRegistration) SWIG_CREATE_PROXY(firebase::firestore::Source) %include "firestore/src/include/firebase/firestore/source.h" +// Generate a C# wrapper for Source. Must be before AggregateQuery. +SWIG_CREATE_PROXY(firebase::firestore::AggregateSource) +%include "firestore/src/include/firebase/firestore/aggregate_source.h" + // Generate a C# wrapper for FieldPath. Must be above DocumentSnapshot and SetOptions. SWIG_CREATE_PROXY(firebase::firestore::FieldPath) %rename("%s") firebase::firestore::FieldPath::FieldPath(const std::vector&); @@ -369,6 +375,7 @@ SWIG_CREATE_PROXY(firebase::firestore::Query); %rename("%s") firebase::firestore::Query::EndBefore(const DocumentSnapshot&) const; %rename("%s") firebase::firestore::Query::EndAt(const DocumentSnapshot&) const; %rename("%s") firebase::firestore::Query::Get; +%rename("%s") firebase::firestore::Query::Count; %rename("%s") firebase::firestore::Query::Direction; %include "firestore/src/include/firebase/firestore/query.h" @@ -387,6 +394,18 @@ SWIG_CREATE_PROXY(firebase::firestore::QuerySnapshot); %rename("%s") firebase::firestore::QuerySnapshot::size; %include "firestore/src/include/firebase/firestore/query_snapshot.h" +// Generate a C# wrapper for AggregateQuery. Must be after Query. +SWIG_CREATE_PROXY(firebase::firestore::AggregateQuery); +%rename("%s") firebase::firestore::AggregateQuery::query; +%rename("%s") firebase::firestore::AggregateQuery::Get; +%include "firestore/src/include/firebase/firestore/aggregate_query.h" + +// Generate a C# wrapper for AggregateQuerySnapshot. Must be after AggregateQuery. +SWIG_CREATE_PROXY(firebase::firestore::AggregateQuerySnapshot); +%rename("%s") firebase::firestore::AggregateQuerySnapshot::query; +%rename("%s") firebase::firestore::AggregateQuerySnapshot::count; +%include "firestore/src/include/firebase/firestore/aggregate_query_snapshot.h" + // Generate a C# wrapper for Settings. SWIG_CREATE_PROXY(firebase::firestore::Settings); %rename("%s") firebase::firestore::Settings::kCacheSizeUnlimited; diff --git a/firestore/src/swig/hash.cc b/firestore/src/swig/hash.cc index 56b2b4ed..6fcfe9ea 100644 --- a/firestore/src/swig/hash.cc +++ b/firestore/src/swig/hash.cc @@ -6,6 +6,12 @@ namespace firestore { // The following functions are declared as friend functions in the corresponding // classes in the C++ SDK headers. +size_t AggregateQueryHash(const AggregateQuery& query) { return query.Hash(); } + +size_t AggregateQuerySnapshotHash(const AggregateQuerySnapshot& snapshot) { + return snapshot.Hash(); +} + size_t QueryHash(const Query& query) { return query.Hash(); } size_t QuerySnapshotHash(const QuerySnapshot& snapshot) { @@ -22,6 +28,20 @@ size_t DocumentChangeHash(const DocumentChange& change) { namespace csharp { +int32_t AggregateQueryHashCode(const AggregateQuery* query) { + if (query == nullptr) { + return 0; + } + return AggregateQueryHash(*query); +} + +int32_t AggregateQuerySnapshotHashCode(const AggregateQuerySnapshot* snapshot) { + if (snapshot == nullptr) { + return 0; + } + return AggregateQuerySnapshotHash(*snapshot); +} + int32_t QueryHashCode(const Query* query) { if (query == nullptr) { return 0; diff --git a/firestore/src/swig/hash.h b/firestore/src/swig/hash.h index c0c23204..7b7916fb 100644 --- a/firestore/src/swig/hash.h +++ b/firestore/src/swig/hash.h @@ -3,6 +3,8 @@ #include +#include "firestore/src/include/firebase/firestore/aggregate_query.h" +#include "firestore/src/include/firebase/firestore/aggregate_query_snapshot.h" #include "firestore/src/include/firebase/firestore/document_change.h" #include "firestore/src/include/firebase/firestore/document_snapshot.h" #include "firestore/src/include/firebase/firestore/query.h" @@ -12,6 +14,12 @@ namespace firebase { namespace firestore { namespace csharp { +// Returns the hash code for the given query. +int32_t AggregateQueryHashCode(const AggregateQuery* query); + +// Returns the hash code for the given query snapshot. +int32_t AggregateQuerySnapshotHashCode(const AggregateQuerySnapshot* snapshot); + // Returns the hash code for the given query. int32_t QueryHashCode(const Query* query); diff --git a/firestore/testapp/Assets/Firebase/Sample/Firestore/UIHandlerAutomated.cs b/firestore/testapp/Assets/Firebase/Sample/Firestore/UIHandlerAutomated.cs index abfd4f35..d9d22734 100644 --- a/firestore/testapp/Assets/Firebase/Sample/Firestore/UIHandlerAutomated.cs +++ b/firestore/testapp/Assets/Firebase/Sample/Firestore/UIHandlerAutomated.cs @@ -142,6 +142,10 @@ protected override void Start() { TestInternalExceptions, TestListenerRegistrationDispose, TestLoadBundles, + TestQueryMethodOnAggregateQuery, + TestQueryMethodOnAggregateQuerySnapshot, + TestAggregateQueryEqualsAndGetHashCode, + TestAggregateQuerySnapshotEqualsAndGetHashCode, TestQueryEqualsAndGetHashCode, TestQuerySnapshotEqualsAndGetHashCode, TestDocumentSnapshotEqualsAndGetHashCode, @@ -3524,6 +3528,25 @@ void VerifyBundledQueryResults(FirebaseFirestore firebaseFirestore) { } } + Task TestQueryMethodOnAggregateQuery() { + return Async(() => { + Query query = TestCollection().WhereEqualTo("num", 1); + + AssertEq(query.Count.Query.Equals(query), true); + }); + } + + Task TestQueryMethodOnAggregateQuerySnapshot() { + return Async(() => { + var collection = TestCollection(); + AggregateQuery query = collection.WhereEqualTo("num", 1).Count; + + AggregateQuerySnapshot snapshot = AssertTaskSucceeds(query.GetSnapshotAsync(AggregateSource.Server)); + + AssertEq(snapshot.Query.Equals(query), true); + }); + } + Task TestQueryEqualsAndGetHashCode() { return Async(() => { var collection = TestCollection(); @@ -3554,6 +3577,37 @@ Task TestQueryEqualsAndGetHashCode() { AssertNe(query6.GetHashCode(), query7.GetHashCode()); }); } + + Task TestAggregateQueryEqualsAndGetHashCode() { + return Async(() => { + var collection = TestCollection(); + + var query1 = collection.WhereEqualTo("num", 1).Count; + var query2 = collection.WhereEqualTo("num", 1).Count; + AssertEq(query1.Equals(query2), true); + AssertEq(query1.GetHashCode(), query2.GetHashCode()); + + var query3 = collection.WhereNotEqualTo("num", 1).Count; + AssertEq(query1.Equals(query3), false); + AssertNe(query1.GetHashCode(), query3.GetHashCode()); + + var query4 = collection.WhereEqualTo("state", "done").Count; + AssertEq(query1.Equals(query4), false); + AssertNe(query1.GetHashCode(), query4.GetHashCode()); + + var query5 = collection.OrderBy("num").Count; + var query6 = collection.Limit(2).Count; + var query7 = collection.OrderBy("num").Limit(2).Count; + AssertEq(query5.Equals(query6), false); + AssertEq(query5.Equals(query7), false); + AssertEq(query6.Equals(query7), false); + AssertEq(query1.Equals(null), false); + + AssertNe(query5.GetHashCode(), query6.GetHashCode()); + AssertNe(query5.GetHashCode(), query7.GetHashCode()); + AssertNe(query6.GetHashCode(), query7.GetHashCode()); + }); + } Task TestQuerySnapshotEqualsAndGetHashCode() { return Async(() => { @@ -3582,6 +3636,28 @@ Task TestQuerySnapshotEqualsAndGetHashCode() { }); } + Task TestAggregateQuerySnapshotEqualsAndGetHashCode() { + return Async(() => { + var collection = TestCollection(); + + AssertTaskSucceeds(collection.Document("a").SetAsync(TestData(1))); + AggregateQuerySnapshot snapshot1 = AssertTaskSucceeds(collection.Count.GetSnapshotAsync(AggregateSource.Server)); + AggregateQuerySnapshot snapshot2 = AssertTaskSucceeds(collection.Count.GetSnapshotAsync(AggregateSource.Server)); + + AssertTaskSucceeds(collection.Document("b").SetAsync(TestData(2))); + AggregateQuerySnapshot snapshot3 = AssertTaskSucceeds(collection.Count.GetSnapshotAsync(AggregateSource.Server)); + + AssertEq(snapshot1.Equals(snapshot1), true); + AssertEq(snapshot1.Equals(snapshot2), true); + AssertEq(snapshot1.Equals(snapshot3), false); + AssertEq(snapshot1.Equals(null), false); + + AssertEq(snapshot1.GetHashCode(), snapshot1.GetHashCode()); + AssertEq(snapshot1.GetHashCode(), snapshot2.GetHashCode()); + AssertNe(snapshot1.GetHashCode(), snapshot3.GetHashCode()); + }); + } + Task TestDocumentSnapshotEqualsAndGetHashCode() { return Async(() => { DocumentReference doc1 = db.Collection("col2").Document(); @@ -3949,12 +4025,19 @@ private void UnityCompileHack() { } private void AssertQueryResults(string desc, Query query, List docIds) { + AssertQueryResults(desc, query, docIds, docIds.Count); + } + + private void AssertQueryResults(string desc, Query query, List docIds, long count) { var snapshot = Await(query.GetSnapshotAsync()); AssertEq(desc + ": Query result count", snapshot.Count, docIds.Count); for (int i = 0; i < snapshot.Count; i++) { AssertEq(desc + ": Document ID " + i, docIds[i], snapshot[i].Id); } + + var aggregateSnapshot = Await(query.Count.GetSnapshotAsync(AggregateSource.Server)); + AssertEq(desc + ": Aggregate query count", aggregateSnapshot.Count, docIds.Count); } private FirebaseFirestore NonDefaultFirestore(string appName) { From c2881cc2790ffda1e17c24791bd4678e81f9459b Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 5 Apr 2023 10:26:55 -0400 Subject: [PATCH 3/4] Add comments and prune imports --- docs/readme.md | 2 ++ firestore/src/AggregateQuery.cs | 15 ++++++++++++++- firestore/src/AggregateQuerySnapshot.cs | 15 ++++++++++----- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/docs/readme.md b/docs/readme.md index f718be04..40e8cc37 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -74,6 +74,8 @@ Release Notes - Messaging (Android): Provide the custom MessagingUnityPlayerActivity as a Java file instead of precompiling it. This is to better support changes with the UnityPlayerActivity in the Unity 2023 editor. + - Firestore: Added `Count()`, which fetches the number of documents in + the result set without actually downloading the documents. ### 10.6.0 - Changes diff --git a/firestore/src/AggregateQuery.cs b/firestore/src/AggregateQuery.cs index 804e3334..7593e703 100644 --- a/firestore/src/AggregateQuery.cs +++ b/firestore/src/AggregateQuery.cs @@ -1,7 +1,6 @@ using Firebase.Firestore.Internal; -using Firebase.Platform; using System.Threading.Tasks; namespace Firebase.Firestore { @@ -20,15 +19,29 @@ internal AggregateQuery(AggregateQueryProxy proxy, FirebaseFirestore firestore) /// public Query Query => new Query(_proxy.query(), _firestore); + /// + /// Asynchronously executes the query. + /// + /// The source from which to acquire the aggregate results. + /// The results of the query. public Task GetSnapshotAsync(AggregateSource source) { var sourceProxy = Enums.Convert(source); return Util.MapResult(_proxy.GetAsync(sourceProxy), taskResult => { return new AggregateQuerySnapshot(taskResult, _firestore); }); } + /// public override bool Equals(object obj) => Equals(obj as Query); + + /// + /// Compares this aggregate query with another for equality. + /// + /// The aggregate query to compare this one with. + /// true if this aggregate query is equal to ; + /// false otherwise. public bool Equals(AggregateQuery other) => other != null && FirestoreCpp.AggregateQueryEquals(_proxy, other._proxy); + /// public override int GetHashCode() { return FirestoreCpp.AggregateQueryHashCode(_proxy); } diff --git a/firestore/src/AggregateQuerySnapshot.cs b/firestore/src/AggregateQuerySnapshot.cs index 7ac7ce17..0ae02624 100644 --- a/firestore/src/AggregateQuerySnapshot.cs +++ b/firestore/src/AggregateQuerySnapshot.cs @@ -12,11 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Collections.Generic; using BclType = System.Type; using Firebase.Firestore.Internal; -using System.Threading.Tasks; namespace Firebase.Firestore { public sealed class AggregateQuerySnapshot { @@ -28,7 +25,7 @@ internal AggregateQuerySnapshot(AggregateQuerySnapshotProxy proxy, FirebaseFires _firestore = Util.NotNull(firestore); } /// - /// The aggregate query producing this snapshot. + /// Returns the query that was executed to produce this result. /// public AggregateQuery Query { get { @@ -36,6 +33,9 @@ public AggregateQuery Query { } } + /// + /// Returns the number of documents in the result set of the underlying query. + /// public long Count { get { return _proxy.count(); @@ -45,7 +45,12 @@ public long Count { /// public override bool Equals(object obj) => Equals(obj as AggregateQuerySnapshot); - /// + /// + /// Compares this aggregate snapshot with another for equality. + /// + /// The aggregate snapshot to compare this one with. + /// true if this aggregate snapshot is equal to ; + /// false otherwise. public bool Equals(AggregateQuerySnapshot other) => other != null && FirestoreCpp.AggregateQuerySnapshotEquals(_proxy, other._proxy); From bac6eab26d80e2f26fd46457a969de0bc4e71351 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 5 Apr 2023 11:41:37 -0400 Subject: [PATCH 4/4] Fix comments --- docs/readme.md | 5 +++-- firestore/src/AggregateQuery.cs | 3 +++ firestore/src/AggregateQuerySnapshot.cs | 4 +++- firestore/src/AggregateSource.cs | 1 - 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/readme.md b/docs/readme.md index b317386a..271ac204 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -78,8 +78,9 @@ Release Notes as a Java file instead of precompiling it. This is to better support changes with the UnityPlayerActivity, and GameActivity options, in the Unity 2023 editor. - - Firestore: Added `Count()`, which fetches the number of documents in - the result set without actually downloading the documents. + - Firestore: Added `Query.Count()`, which fetches the number of documents in + the result set without actually downloading the documents + ([#659](https://github.com/firebase/firebase-unity-sdk/pull/659)). ### 10.6.0 - Changes diff --git a/firestore/src/AggregateQuery.cs b/firestore/src/AggregateQuery.cs index 7593e703..e6ca8750 100644 --- a/firestore/src/AggregateQuery.cs +++ b/firestore/src/AggregateQuery.cs @@ -4,6 +4,9 @@ using System.Threading.Tasks; namespace Firebase.Firestore { + /// + /// A query that calculates aggregations over an underlying query. + /// public sealed class AggregateQuery { private readonly AggregateQueryProxy _proxy; diff --git a/firestore/src/AggregateQuerySnapshot.cs b/firestore/src/AggregateQuerySnapshot.cs index 0ae02624..c3d15eaa 100644 --- a/firestore/src/AggregateQuerySnapshot.cs +++ b/firestore/src/AggregateQuerySnapshot.cs @@ -12,10 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -using BclType = System.Type; using Firebase.Firestore.Internal; namespace Firebase.Firestore { + /// + /// The results of executing an AggregateQuerySnapshot. + /// public sealed class AggregateQuerySnapshot { private readonly AggregateQuerySnapshotProxy _proxy; private readonly FirebaseFirestore _firestore; diff --git a/firestore/src/AggregateSource.cs b/firestore/src/AggregateSource.cs index 8fe670c4..872a9ca4 100644 --- a/firestore/src/AggregateSource.cs +++ b/firestore/src/AggregateSource.cs @@ -18,7 +18,6 @@ namespace Firebase.Firestore { /// The sources from which an can retrieve its /// results. /// - public enum AggregateSource { /// Perform the aggregation on the server and download the result. /// The result received from the server is presented, unaltered, without