From d5bf030b6f52c367b54e787352b86a23ef188c8b Mon Sep 17 00:00:00 2001 From: Preston Date: Fri, 3 Oct 2025 12:00:21 -0500 Subject: [PATCH 1/3] Added support for the count() aggregation query. --- packages/dart_firebase_admin/README.md | 3 + .../src/google_cloud_firestore/reference.dart | 181 ++++++++++++++++ .../aggregate_query_test.dart | 194 ++++++++++++++++++ pubspec.yaml | 1 + 4 files changed, 379 insertions(+) create mode 100644 packages/dart_firebase_admin/test/google_cloud_firestore/aggregate_query_test.dart diff --git a/packages/dart_firebase_admin/README.md b/packages/dart_firebase_admin/README.md index a94c7da..05deb2d 100644 --- a/packages/dart_firebase_admin/README.md +++ b/packages/dart_firebase_admin/README.md @@ -202,6 +202,9 @@ print(user.data()?['age']); | query.limit | ✅ | | query.limitToLast | ✅ | | query.offset | ✅ | +| query.count() | ✅ | +| query.sum() | ❌ | +| query.average() | ❌ | | querySnapshot.docs | ✅ | | querySnapshot.readTime | ✅ | | documentSnapshots.data | ✅ | diff --git a/packages/dart_firebase_admin/lib/src/google_cloud_firestore/reference.dart b/packages/dart_firebase_admin/lib/src/google_cloud_firestore/reference.dart index 3157278..bac1ff6 100644 --- a/packages/dart_firebase_admin/lib/src/google_cloud_firestore/reference.dart +++ b/packages/dart_firebase_admin/lib/src/google_cloud_firestore/reference.dart @@ -1680,6 +1680,187 @@ base class Query { @override int get hashCode => Object.hash(runtimeType, _queryOptions); + + /// Returns an [AggregateQuery] that can be used to execute a count + /// aggregation. + /// + /// The returned query, when executed, counts the documents in the result + /// set of this query without actually downloading the documents. + /// + /// ```dart + /// firestore.collection('cities').count().get().then( + /// (res) => print(res.count), + /// onError: (e) => print('Error completing: $e'), + /// ); + /// ``` + AggregateQuery count() { + return AggregateQuery._( + query: this, + aggregations: [ + _AggregateField( + alias: 'count', + aggregation: firestore1.Aggregation( + count: firestore1.Count(), + ), + ), + ], + ); + } +} + +/// Represents an aggregation field to use in an aggregation query. +@immutable +class _AggregateField { + const _AggregateField({ + required this.alias, + required this.aggregation, + }); + + final String alias; + final firestore1.Aggregation aggregation; + + @override + bool operator ==(Object other) { + return other is _AggregateField && + alias == other.alias && + // For count aggregations, we just check that both have count set + ((aggregation.count != null && other.aggregation.count != null) || + aggregation == other.aggregation); + } + + @override + int get hashCode => Object.hash(alias, aggregation.count != null); +} + +/// Calculates aggregations over an underlying query. +@immutable +class AggregateQuery { + const AggregateQuery._({ + required this.query, + required this.aggregations, + }); + + /// The query whose aggregations will be calculated by this object. + final Query query; + + // For now, only supporting a single aggregation (count). + // But storing as a list for future multi aggregate queries. + @internal + final List<_AggregateField> aggregations; + + /// Executes the aggregate query and returns the results as an + /// [AggregateQuerySnapshot]. + /// + /// ```dart + /// firestore.collection('cities').count().get().then( + /// (res) => print(res.count), + /// onError: (e) => print('Error completing: $e'), + /// ); + /// ``` + Future get() async { + final firestore = query.firestore; + + final aggregationQuery = firestore1.RunAggregationQueryRequest( + structuredAggregationQuery: firestore1.StructuredAggregationQuery( + structuredQuery: query._toStructuredQuery(), + aggregations: [ + for (final field in aggregations) + firestore1.Aggregation( + alias: field.alias, + count: field.aggregation.count, + ), + ], + ), + ); + + final response = await firestore._client.v1((client) async { + return client.projects.databases.documents.runAggregationQuery( + aggregationQuery, + query._buildProtoParentPath(), + ); + }); + + final results = {}; + Timestamp? readTime; + + for (final result in response) { + if (result.result != null && result.result!.aggregateFields != null) { + for (final entry in result.result!.aggregateFields!.entries) { + final value = entry.value; + if (value.integerValue != null) { + results[entry.key] = int.parse(value.integerValue!); + } else if (value.doubleValue != null) { + results[entry.key] = value.doubleValue; + } else if (value.nullValue != null) { + results[entry.key] = null; + } + } + } + + if (result.readTime != null) { + readTime = Timestamp._fromString(result.readTime!); + } + } + + return AggregateQuerySnapshot._( + query: this, + readTime: readTime, + data: results, + ); + } + + @override + bool operator ==(Object other) { + return other is AggregateQuery && + query == other.query && + const ListEquality<_AggregateField>() + .equals(aggregations, other.aggregations); + } + + @override + int get hashCode => Object.hash( + query, + const ListEquality<_AggregateField>().hash(aggregations), + ); +} + +/// The results of executing an aggregation query. +@immutable +class AggregateQuerySnapshot { + const AggregateQuerySnapshot._({ + required this.query, + required this.readTime, + required this.data, + }); + + /// The query that was executed to produce this result. + final AggregateQuery query; + + /// The time this snapshot was obtained. + final Timestamp? readTime; + + /// The raw aggregation data, keyed by alias. + final Map data; + + /// The count of documents that match the query. Returns `null` if the + /// count aggregation was not performed. + int? get count => data['count'] as int?; + + /// Gets an aggregate field by alias. + /// + /// - [alias]: The alias of the aggregate field to retrieve. + Object? getField(String alias) => data[alias]; + + @override + bool operator ==(Object other) { + return other is AggregateQuerySnapshot && + query == other.query && + readTime == other.readTime && + const MapEquality().equals(data, other.data); + } + + @override + int get hashCode => Object.hash(query, readTime, data); } /// A QuerySnapshot contains zero or more [QueryDocumentSnapshot] objects diff --git a/packages/dart_firebase_admin/test/google_cloud_firestore/aggregate_query_test.dart b/packages/dart_firebase_admin/test/google_cloud_firestore/aggregate_query_test.dart new file mode 100644 index 0000000..ba11f20 --- /dev/null +++ b/packages/dart_firebase_admin/test/google_cloud_firestore/aggregate_query_test.dart @@ -0,0 +1,194 @@ +import 'package:dart_firebase_admin/firestore.dart'; +import 'package:test/test.dart'; + +import 'util/helpers.dart'; + +void main() { + group('AggregateQuery', () { + late Firestore firestore; + late CollectionReference collection; + + setUp(() async { + firestore = await createFirestore(); + collection = firestore.collection( + 'aggregate-test-${DateTime.now().millisecondsSinceEpoch}'); + }); + + test('count() on empty collection returns 0', () async { + final query = collection.where('foo', WhereFilter.equal, 'bar'); + final aggregateQuery = query.count(); + final snapshot = await aggregateQuery.get(); + + expect(snapshot.count, 0); + }); + + test('count() returns correct count for matching documents', () async { + // Add some test documents + await collection.add({'name': 'Alice', 'age': 30}); + await collection.add({'name': 'Bob', 'age': 25}); + await collection.add({'name': 'Charlie', 'age': 30}); + await collection.add({'name': 'David', 'age': 35}); + + // Test count without filter + final allCount = await collection.count().get(); + expect(allCount.count, 4); + + // Test count with filter + final filtered = + await collection.where('age', WhereFilter.equal, 30).count().get(); + expect(filtered.count, 2); + }); + + test('count() works with complex queries', () async { + // Add test documents + await collection + .add({'category': 'books', 'price': 15.99, 'inStock': true}); + await collection + .add({'category': 'books', 'price': 25.99, 'inStock': false}); + await collection + .add({'category': 'books', 'price': 9.99, 'inStock': true}); + await collection + .add({'category': 'electronics', 'price': 199.99, 'inStock': true}); + await collection + .add({'category': 'electronics', 'price': 299.99, 'inStock': false}); + + // Test with multiple where conditions + final query = collection + .where('category', WhereFilter.equal, 'books') + .where('inStock', WhereFilter.equal, true); + final count = await query.count().get(); + expect(count.count, 2); + + // Test with range query + final rangeQuery = collection + .where('price', WhereFilter.greaterThanOrEqual, 20) + .where('price', WhereFilter.lessThan, 200); + final rangeCount = await rangeQuery.count().get(); + expect(rangeCount.count, 2); + }); + + test('count() works with orderBy and limit', () async { + // Add test documents + for (var i = 1; i <= 10; i++) { + await collection.add({'value': i}); + } + + // Test with limit + final limitQuery = collection.orderBy('value').limit(5); + final limitCount = await limitQuery.count().get(); + expect(limitCount.count, 5); + + // Test with limitToLast + final limitToLastQuery = collection.orderBy('value').limitToLast(3); + final limitToLastCount = await limitToLastQuery.count().get(); + expect(limitToLastCount.count, 3); + }); + + test('count() works with startAt and endAt', () async { + // Add test documents + for (var i = 1; i <= 10; i++) { + await collection.add({'value': i}); + } + + // Test with startAt + final startAtQuery = collection.orderBy('value').startAt([5]); + final startAtCount = await startAtQuery.count().get(); + expect(startAtCount.count, 6); // values 5-10 + + // Test with endBefore + final endBeforeQuery = collection.orderBy('value').endBefore([7]); + final endBeforeCount = await endBeforeQuery.count().get(); + expect(endBeforeCount.count, 6); // values 1-6 + + // Test with both startAfter and endAt + final rangeQuery = collection.orderBy('value').startAfter([3]).endAt([8]); + final rangeCount = await rangeQuery.count().get(); + expect(rangeCount.count, 5); // values 4-8 + }); + + test('count() works with collection groups', () async { + // Create documents with subcollections + final doc1 = collection.doc('doc1'); + final doc2 = collection.doc('doc2'); + + await doc1.set({'type': 'parent'}); + await doc2.set({'type': 'parent'}); + + await doc1.collection('items').add({'name': 'item1'}); + await doc1.collection('items').add({'name': 'item2'}); + await doc2.collection('items').add({'name': 'item3'}); + + // Count collection group + final collectionGroup = firestore.collectionGroup('items'); + final groupCount = await collectionGroup.count().get(); + expect(groupCount.count, 3); + }); + + test('AggregateQuerySnapshot provides readTime', () async { + await collection.add({'test': true}); + + final snapshot = await collection.count().get(); + expect(snapshot.count, 1); + expect(snapshot.readTime, isNotNull); + }); + + test('AggregateQuery equality', () { + final query1 = collection.where('foo', WhereFilter.equal, 'bar'); + final query2 = collection.where('foo', WhereFilter.equal, 'bar'); + final query3 = collection.where('foo', WhereFilter.equal, 'baz'); + + final aggregate1 = query1.count(); + final aggregate2 = query2.count(); + final aggregate3 = query3.count(); + + expect(aggregate1, equals(aggregate2)); + expect(aggregate1, isNot(equals(aggregate3))); + expect(aggregate1.hashCode, equals(aggregate2.hashCode)); + }); + + test('AggregateQuerySnapshot equality', () async { + await collection.add({'test': true}); + + final aggregate = collection.count(); + final snapshot1 = await aggregate.get(); + final snapshot2 = await aggregate.get(); + + // Note: These won't be equal because readTime will be different + // But we can test the structure + expect(snapshot1.query, equals(snapshot2.query)); + expect(snapshot1.count, equals(snapshot2.count)); + }); + + test('count() with composite filters', () async { + await collection.add({'a': 1, 'b': 'x'}); + await collection.add({'a': 2, 'b': 'y'}); + await collection.add({'a': 3, 'b': 'x'}); + await collection.add({'a': 1, 'b': 'y'}); + + // Test AND filter + final andFilter = Filter.and([ + Filter.where('a', WhereFilter.greaterThan, 1), + Filter.where('b', WhereFilter.equal, 'x'), + ]); + final andCount = await collection.whereFilter(andFilter).count().get(); + expect(andCount.count, 1); // Only {a: 3, b: 'x'} matches + + // Test OR filter + final orFilter = Filter.or([ + Filter.where('a', WhereFilter.equal, 1), + Filter.where('b', WhereFilter.equal, 'y'), + ]); + final orCount = await collection.whereFilter(orFilter).count().get(); + expect( + orCount.count, 3); // {a: 1, b: 'x'}, {a: 2, b: 'y'}, {a: 1, b: 'y'} + }); + + test('getField() returns correct values', () async { + await collection.add({'test': true}); + + final snapshot = await collection.count().get(); + expect(snapshot.getField('count'), equals(snapshot.count)); + expect(snapshot.getField('nonexistent'), isNull); + }); + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index 255837e..6a0028d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,3 +5,4 @@ environment: sdk: '>=3.0.0 <4.0.0' dev_dependencies: melos: ^6.1.0 + test: ^1.26.3 From dca072ae3043e884a27cf6dc95fcab8b773fb223 Mon Sep 17 00:00:00 2001 From: Preston Date: Fri, 3 Oct 2025 13:44:32 -0500 Subject: [PATCH 2/3] Added support for sum, average, and running multiple aggregate queries at once. --- .../src/google_cloud_firestore/reference.dart | 220 ++++++++++++++++-- .../aggregate_query_test.dart | 55 +++++ 2 files changed, 255 insertions(+), 20 deletions(-) diff --git a/packages/dart_firebase_admin/lib/src/google_cloud_firestore/reference.dart b/packages/dart_firebase_admin/lib/src/google_cloud_firestore/reference.dart index bac1ff6..2bc9099 100644 --- a/packages/dart_firebase_admin/lib/src/google_cloud_firestore/reference.dart +++ b/packages/dart_firebase_admin/lib/src/google_cloud_firestore/reference.dart @@ -1681,6 +1681,48 @@ base class Query { @override int get hashCode => Object.hash(runtimeType, _queryOptions); + /// Returns an [AggregateQuery] that can be used to execute one or more + /// aggregation queries over the result set of this query. + /// + /// ## Limitations + /// - Aggregation queries are only supported through direct server response + /// - Cannot be used with real-time listeners or offline queries + /// - Must complete within 60 seconds or returns DEADLINE_EXCEEDED error + /// - For sum() and average(), non-numeric values are ignored + /// - When combining aggregations on different fields, only documents + /// containing all those fields are included + /// + /// ```dart + /// firestore.collection('cities').aggregate( + /// AggregateField.count(), + /// AggregateField.sum('population'), + /// AggregateField.average('population'), + /// ).get().then( + /// (res) { + /// print(res.count); + /// print(res.getSum('population')); + /// print(res.getAverage('population')); + /// }, + /// onError: (e) => print('Error completing: $e'), + /// ); + /// ``` + AggregateQuery aggregate( + AggregateField aggregateField1, [ + AggregateField? aggregateField2, + AggregateField? aggregateField3, + ]) { + final fields = [ + aggregateField1, + if (aggregateField2 != null) aggregateField2, + if (aggregateField3 != null) aggregateField3, + ]; + + return AggregateQuery._( + query: this, + aggregations: fields.map((field) => field._toInternal()).toList(), + ); + } + /// Returns an [AggregateQuery] that can be used to execute a count /// aggregation. /// @@ -1694,24 +1736,129 @@ base class Query { /// ); /// ``` AggregateQuery count() { - return AggregateQuery._( - query: this, - aggregations: [ - _AggregateField( - alias: 'count', - aggregation: firestore1.Aggregation( - count: firestore1.Count(), + return aggregate(AggregateField.count()); + } +} + +/// Defines an aggregation that can be performed by Firestore. +@immutable +class AggregateField { + const AggregateField._({ + required this.fieldPath, + required this.alias, + required this.type, + }); + + /// The field to aggregate on, or null for count aggregations. + final String? fieldPath; + + /// The alias to use for this aggregation result. + final String alias; + + /// The type of aggregation. + final _AggregateType type; + + /// Creates a count aggregation. + /// + /// Count aggregations provide the number of documents that match the query. + /// The result can be accessed using [AggregateQuerySnapshot.count]. + factory AggregateField.count() { + return const AggregateField._( + fieldPath: null, + alias: 'count', + type: _AggregateType.count, + ); + } + + /// Creates a sum aggregation for the specified field. + /// + /// - [field]: The field to sum across all matching documents. + /// + /// The result can be accessed using [AggregateQuerySnapshot.getSum]. + factory AggregateField.sum(String field) { + return AggregateField._( + fieldPath: field, + alias: 'sum_$field', + type: _AggregateType.sum, + ); + } + + /// Creates an average aggregation for the specified field. + /// + /// - [field]: The field to average across all matching documents. + /// + /// The result can be accessed using [AggregateQuerySnapshot.getAverage]. + factory AggregateField.average(String field) { + return AggregateField._( + fieldPath: field, + alias: 'avg_$field', + type: _AggregateType.average, + ); + } + + /// Converts this public field to the internal representation. + _AggregateFieldInternal _toInternal() { + firestore1.Aggregation aggregation; + switch (type) { + case _AggregateType.count: + aggregation = firestore1.Aggregation( + count: firestore1.Count(), + ); + break; + case _AggregateType.sum: + aggregation = firestore1.Aggregation( + sum: firestore1.Sum( + field: firestore1.FieldReference(fieldPath: fieldPath!), ), - ), - ], + ); + break; + case _AggregateType.average: + aggregation = firestore1.Aggregation( + avg: firestore1.Avg( + field: firestore1.FieldReference(fieldPath: fieldPath!), + ), + ); + break; + } + + return _AggregateFieldInternal( + alias: alias, + aggregation: aggregation, ); } } -/// Represents an aggregation field to use in an aggregation query. +/// Creates a count aggregation. +/// +/// Count aggregations provide the number of documents that match the query. +/// The result can be accessed using [AggregateQuerySnapshot.count]. +AggregateField count() => AggregateField.count(); + +/// Creates a sum aggregation for the specified field. +/// +/// - [field]: The field to sum across all matching documents. +/// +/// The result can be accessed using [AggregateQuerySnapshot.getSum]. +AggregateField sum(String field) => AggregateField.sum(field); + +/// Creates an average aggregation for the specified field. +/// +/// - [field]: The field to average across all matching documents. +/// +/// The result can be accessed using [AggregateQuerySnapshot.getAverage]. +AggregateField average(String field) => AggregateField.average(field); + +/// The type of aggregation to perform. +enum _AggregateType { + count, + sum, + average, +} + +/// Internal representation of an aggregation field. @immutable -class _AggregateField { - const _AggregateField({ +class _AggregateFieldInternal { + const _AggregateFieldInternal({ required this.alias, required this.aggregation, }); @@ -1721,15 +1868,19 @@ class _AggregateField { @override bool operator ==(Object other) { - return other is _AggregateField && + return other is _AggregateFieldInternal && alias == other.alias && // For count aggregations, we just check that both have count set ((aggregation.count != null && other.aggregation.count != null) || - aggregation == other.aggregation); + (aggregation.sum != null && other.aggregation.sum != null) || + (aggregation.avg != null && other.aggregation.avg != null)); } @override - int get hashCode => Object.hash(alias, aggregation.count != null); + int get hashCode => Object.hash( + alias, + aggregation.count != null || aggregation.sum != null || aggregation.avg != null, + ); } /// Calculates aggregations over an underlying query. @@ -1743,10 +1894,8 @@ class AggregateQuery { /// The query whose aggregations will be calculated by this object. final Query query; - // For now, only supporting a single aggregation (count). - // But storing as a list for future multi aggregate queries. @internal - final List<_AggregateField> aggregations; + final List<_AggregateFieldInternal> aggregations; /// Executes the aggregate query and returns the results as an /// [AggregateQuerySnapshot]. @@ -1768,6 +1917,8 @@ class AggregateQuery { firestore1.Aggregation( alias: field.alias, count: field.aggregation.count, + sum: field.aggregation.sum, + avg: field.aggregation.avg, ), ], ), @@ -1813,14 +1964,14 @@ class AggregateQuery { bool operator ==(Object other) { return other is AggregateQuery && query == other.query && - const ListEquality<_AggregateField>() + const ListEquality<_AggregateFieldInternal>() .equals(aggregations, other.aggregations); } @override int get hashCode => Object.hash( query, - const ListEquality<_AggregateField>().hash(aggregations), + const ListEquality<_AggregateFieldInternal>().hash(aggregations), ); } @@ -1846,6 +1997,35 @@ class AggregateQuerySnapshot { /// count aggregation was not performed. int? get count => data['count'] as int?; + /// Gets the sum for the specified field. Returns `null` if the + /// sum aggregation was not performed. + /// + /// - [field]: The field that was summed. + num? getSum(String field) { + final alias = 'sum_$field'; + final value = data[alias]; + if (value == null) return null; + if (value is int || value is double) return value as num; + // Handle case where sum might be returned as a string + if (value is String) return num.tryParse(value); + return null; + } + + /// Gets the average for the specified field. Returns `null` if the + /// average aggregation was not performed. + /// + /// - [field]: The field that was averaged. + double? getAverage(String field) { + final alias = 'avg_$field'; + final value = data[alias]; + if (value == null) return null; + if (value is double) return value; + if (value is int) return value.toDouble(); + // Handle case where average might be returned as a string + if (value is String) return double.tryParse(value); + return null; + } + /// Gets an aggregate field by alias. /// /// - [alias]: The alias of the aggregate field to retrieve. diff --git a/packages/dart_firebase_admin/test/google_cloud_firestore/aggregate_query_test.dart b/packages/dart_firebase_admin/test/google_cloud_firestore/aggregate_query_test.dart index ba11f20..13067df 100644 --- a/packages/dart_firebase_admin/test/google_cloud_firestore/aggregate_query_test.dart +++ b/packages/dart_firebase_admin/test/google_cloud_firestore/aggregate_query_test.dart @@ -190,5 +190,60 @@ void main() { expect(snapshot.getField('count'), equals(snapshot.count)); expect(snapshot.getField('nonexistent'), isNull); }); + + test('sum() aggregation works correctly', () async { + await collection.add({'price': 10.5}); + await collection.add({'price': 20.0}); + await collection.add({'price': 15.5}); + + final snapshot = await collection.aggregate( + sum('price'), + ).get(); + + expect(snapshot.getSum('price'), equals(46.0)); + }); + + test('average() aggregation works correctly', () async { + await collection.add({'score': 80}); + await collection.add({'score': 90}); + await collection.add({'score': 100}); + + final snapshot = await collection.aggregate( + average('score'), + ).get(); + + expect(snapshot.getAverage('score'), equals(90.0)); + }); + + test('multiple aggregations work together', () async { + await collection.add({'value': 10, 'category': 'A'}); + await collection.add({'value': 20, 'category': 'A'}); + await collection.add({'value': 30, 'category': 'B'}); + await collection.add({'value': 40, 'category': 'B'}); + + final snapshot = await collection + .where('category', WhereFilter.equal, 'A') + .aggregate( + count(), + sum('value'), + average('value'), + ) + .get(); + + expect(snapshot.count, equals(2)); + expect(snapshot.getSum('value'), equals(30)); + expect(snapshot.getAverage('value'), equals(15.0)); + }); + + test('aggregate() with single field works', () async { + await collection.add({'amount': 100}); + await collection.add({'amount': 200}); + + final snapshot = await collection.aggregate( + count(), + ).get(); + + expect(snapshot.count, equals(2)); + }); }); } From 8a159ec9a35d2dd8a4adbb8db5744a03a4d58e46 Mon Sep 17 00:00:00 2001 From: Preston Date: Fri, 3 Oct 2025 14:11:43 -0500 Subject: [PATCH 3/3] Fixed naming convention for the count, sum, and average classes. --- .../src/google_cloud_firestore/reference.dart | 68 ++++++++++++------- 1 file changed, 45 insertions(+), 23 deletions(-) diff --git a/packages/dart_firebase_admin/lib/src/google_cloud_firestore/reference.dart b/packages/dart_firebase_admin/lib/src/google_cloud_firestore/reference.dart index 2bc9099..0d759c7 100644 --- a/packages/dart_firebase_admin/lib/src/google_cloud_firestore/reference.dart +++ b/packages/dart_firebase_admin/lib/src/google_cloud_firestore/reference.dart @@ -1694,9 +1694,9 @@ base class Query { /// /// ```dart /// firestore.collection('cities').aggregate( - /// AggregateField.count(), - /// AggregateField.sum('population'), - /// AggregateField.average('population'), + /// count(), + /// sum('population'), + /// average('population'), /// ).get().then( /// (res) { /// print(res.count); @@ -1828,26 +1828,6 @@ class AggregateField { } } -/// Creates a count aggregation. -/// -/// Count aggregations provide the number of documents that match the query. -/// The result can be accessed using [AggregateQuerySnapshot.count]. -AggregateField count() => AggregateField.count(); - -/// Creates a sum aggregation for the specified field. -/// -/// - [field]: The field to sum across all matching documents. -/// -/// The result can be accessed using [AggregateQuerySnapshot.getSum]. -AggregateField sum(String field) => AggregateField.sum(field); - -/// Creates an average aggregation for the specified field. -/// -/// - [field]: The field to average across all matching documents. -/// -/// The result can be accessed using [AggregateQuerySnapshot.getAverage]. -AggregateField average(String field) => AggregateField.average(field); - /// The type of aggregation to perform. enum _AggregateType { count, @@ -1855,6 +1835,48 @@ enum _AggregateType { average, } +/// Create a CountAggregateField object that can be used to compute +/// the count of documents in the result set of a query. +// ignore: camel_case_types +class count extends AggregateField { + /// Creates a count aggregation. + const count() : super._( + fieldPath: null, + alias: 'count', + type: _AggregateType.count, + ); +} + +/// Create an object that can be used to compute the sum of a specified field +/// over a range of documents in the result set of a query. +// ignore: camel_case_types +class sum extends AggregateField { + /// Creates a sum aggregation for the specified field. + sum(this.field) : super._( + fieldPath: field, + alias: 'sum_$field', + type: _AggregateType.sum, + ); + + /// The field to sum. + final String field; +} + +/// Create an object that can be used to compute the average of a specified field +/// over a range of documents in the result set of a query. +// ignore: camel_case_types +class average extends AggregateField { + /// Creates an average aggregation for the specified field. + average(this.field) : super._( + fieldPath: field, + alias: 'avg_$field', + type: _AggregateType.average, + ); + + /// The field to average. + final String field; +} + /// Internal representation of an aggregation field. @immutable class _AggregateFieldInternal {