@@ -1680,6 +1680,389 @@ base class Query<T> {
16801680
16811681 @override
16821682 int get hashCode => Object .hash (runtimeType, _queryOptions);
1683+
1684+ /// Returns an [AggregateQuery] that can be used to execute one or more
1685+ /// aggregation queries over the result set of this query.
1686+ ///
1687+ /// ## Limitations
1688+ /// - Aggregation queries are only supported through direct server response
1689+ /// - Cannot be used with real-time listeners or offline queries
1690+ /// - Must complete within 60 seconds or returns DEADLINE_EXCEEDED error
1691+ /// - For sum() and average(), non-numeric values are ignored
1692+ /// - When combining aggregations on different fields, only documents
1693+ /// containing all those fields are included
1694+ ///
1695+ /// ```dart
1696+ /// firestore.collection('cities').aggregate(
1697+ /// count(),
1698+ /// sum('population'),
1699+ /// average('population'),
1700+ /// ).get().then(
1701+ /// (res) {
1702+ /// print(res.count);
1703+ /// print(res.getSum('population'));
1704+ /// print(res.getAverage('population'));
1705+ /// },
1706+ /// onError: (e) => print('Error completing: $e'),
1707+ /// );
1708+ /// ```
1709+ AggregateQuery aggregate (
1710+ AggregateField aggregateField1, [
1711+ AggregateField ? aggregateField2,
1712+ AggregateField ? aggregateField3,
1713+ ]) {
1714+ final fields = [
1715+ aggregateField1,
1716+ if (aggregateField2 != null ) aggregateField2,
1717+ if (aggregateField3 != null ) aggregateField3,
1718+ ];
1719+
1720+ return AggregateQuery ._(
1721+ query: this ,
1722+ aggregations: fields.map ((field) => field._toInternal ()).toList (),
1723+ );
1724+ }
1725+
1726+ /// Returns an [AggregateQuery] that can be used to execute a count
1727+ /// aggregation.
1728+ ///
1729+ /// The returned query, when executed, counts the documents in the result
1730+ /// set of this query without actually downloading the documents.
1731+ ///
1732+ /// ```dart
1733+ /// firestore.collection('cities').count().get().then(
1734+ /// (res) => print(res.count),
1735+ /// onError: (e) => print('Error completing: $e'),
1736+ /// );
1737+ /// ```
1738+ AggregateQuery count () {
1739+ return aggregate (AggregateField .count ());
1740+ }
1741+ }
1742+
1743+ /// Defines an aggregation that can be performed by Firestore.
1744+ @immutable
1745+ class AggregateField {
1746+ const AggregateField ._({
1747+ required this .fieldPath,
1748+ required this .alias,
1749+ required this .type,
1750+ });
1751+
1752+ /// The field to aggregate on, or null for count aggregations.
1753+ final String ? fieldPath;
1754+
1755+ /// The alias to use for this aggregation result.
1756+ final String alias;
1757+
1758+ /// The type of aggregation.
1759+ final _AggregateType type;
1760+
1761+ /// Creates a count aggregation.
1762+ ///
1763+ /// Count aggregations provide the number of documents that match the query.
1764+ /// The result can be accessed using [AggregateQuerySnapshot.count] .
1765+ factory AggregateField .count () {
1766+ return const AggregateField ._(
1767+ fieldPath: null ,
1768+ alias: 'count' ,
1769+ type: _AggregateType .count,
1770+ );
1771+ }
1772+
1773+ /// Creates a sum aggregation for the specified field.
1774+ ///
1775+ /// - [field] : The field to sum across all matching documents.
1776+ ///
1777+ /// The result can be accessed using [AggregateQuerySnapshot.getSum] .
1778+ factory AggregateField .sum (String field) {
1779+ return AggregateField ._(
1780+ fieldPath: field,
1781+ alias: 'sum_$field ' ,
1782+ type: _AggregateType .sum,
1783+ );
1784+ }
1785+
1786+ /// Creates an average aggregation for the specified field.
1787+ ///
1788+ /// - [field] : The field to average across all matching documents.
1789+ ///
1790+ /// The result can be accessed using [AggregateQuerySnapshot.getAverage] .
1791+ factory AggregateField .average (String field) {
1792+ return AggregateField ._(
1793+ fieldPath: field,
1794+ alias: 'avg_$field ' ,
1795+ type: _AggregateType .average,
1796+ );
1797+ }
1798+
1799+ /// Converts this public field to the internal representation.
1800+ _AggregateFieldInternal _toInternal () {
1801+ firestore1.Aggregation aggregation;
1802+ switch (type) {
1803+ case _AggregateType .count:
1804+ aggregation = firestore1.Aggregation (
1805+ count: firestore1.Count (),
1806+ );
1807+ break ;
1808+ case _AggregateType .sum:
1809+ aggregation = firestore1.Aggregation (
1810+ sum: firestore1.Sum (
1811+ field: firestore1.FieldReference (fieldPath: fieldPath! ),
1812+ ),
1813+ );
1814+ break ;
1815+ case _AggregateType .average:
1816+ aggregation = firestore1.Aggregation (
1817+ avg: firestore1.Avg (
1818+ field: firestore1.FieldReference (fieldPath: fieldPath! ),
1819+ ),
1820+ );
1821+ break ;
1822+ }
1823+
1824+ return _AggregateFieldInternal (
1825+ alias: alias,
1826+ aggregation: aggregation,
1827+ );
1828+ }
1829+ }
1830+
1831+ /// The type of aggregation to perform.
1832+ enum _AggregateType {
1833+ count,
1834+ sum,
1835+ average,
1836+ }
1837+
1838+ /// Create a CountAggregateField object that can be used to compute
1839+ /// the count of documents in the result set of a query.
1840+ // ignore: camel_case_types
1841+ class count extends AggregateField {
1842+ /// Creates a count aggregation.
1843+ const count () : super ._(
1844+ fieldPath: null ,
1845+ alias: 'count' ,
1846+ type: _AggregateType .count,
1847+ );
1848+ }
1849+
1850+ /// Create an object that can be used to compute the sum of a specified field
1851+ /// over a range of documents in the result set of a query.
1852+ // ignore: camel_case_types
1853+ class sum extends AggregateField {
1854+ /// Creates a sum aggregation for the specified field.
1855+ sum (this .field) : super ._(
1856+ fieldPath: field,
1857+ alias: 'sum_$field ' ,
1858+ type: _AggregateType .sum,
1859+ );
1860+
1861+ /// The field to sum.
1862+ final String field;
1863+ }
1864+
1865+ /// Create an object that can be used to compute the average of a specified field
1866+ /// over a range of documents in the result set of a query.
1867+ // ignore: camel_case_types
1868+ class average extends AggregateField {
1869+ /// Creates an average aggregation for the specified field.
1870+ average (this .field) : super ._(
1871+ fieldPath: field,
1872+ alias: 'avg_$field ' ,
1873+ type: _AggregateType .average,
1874+ );
1875+
1876+ /// The field to average.
1877+ final String field;
1878+ }
1879+
1880+ /// Internal representation of an aggregation field.
1881+ @immutable
1882+ class _AggregateFieldInternal {
1883+ const _AggregateFieldInternal ({
1884+ required this .alias,
1885+ required this .aggregation,
1886+ });
1887+
1888+ final String alias;
1889+ final firestore1.Aggregation aggregation;
1890+
1891+ @override
1892+ bool operator == (Object other) {
1893+ return other is _AggregateFieldInternal &&
1894+ alias == other.alias &&
1895+ // For count aggregations, we just check that both have count set
1896+ ((aggregation.count != null && other.aggregation.count != null ) ||
1897+ (aggregation.sum != null && other.aggregation.sum != null ) ||
1898+ (aggregation.avg != null && other.aggregation.avg != null ));
1899+ }
1900+
1901+ @override
1902+ int get hashCode => Object .hash (
1903+ alias,
1904+ aggregation.count != null || aggregation.sum != null || aggregation.avg != null ,
1905+ );
1906+ }
1907+
1908+ /// Calculates aggregations over an underlying query.
1909+ @immutable
1910+ class AggregateQuery {
1911+ const AggregateQuery ._({
1912+ required this .query,
1913+ required this .aggregations,
1914+ });
1915+
1916+ /// The query whose aggregations will be calculated by this object.
1917+ final Query <Object ?> query;
1918+
1919+ @internal
1920+ final List <_AggregateFieldInternal > aggregations;
1921+
1922+ /// Executes the aggregate query and returns the results as an
1923+ /// [AggregateQuerySnapshot] .
1924+ ///
1925+ /// ```dart
1926+ /// firestore.collection('cities').count().get().then(
1927+ /// (res) => print(res.count),
1928+ /// onError: (e) => print('Error completing: $e'),
1929+ /// );
1930+ /// ```
1931+ Future <AggregateQuerySnapshot > get () async {
1932+ final firestore = query.firestore;
1933+
1934+ final aggregationQuery = firestore1.RunAggregationQueryRequest (
1935+ structuredAggregationQuery: firestore1.StructuredAggregationQuery (
1936+ structuredQuery: query._toStructuredQuery (),
1937+ aggregations: [
1938+ for (final field in aggregations)
1939+ firestore1.Aggregation (
1940+ alias: field.alias,
1941+ count: field.aggregation.count,
1942+ sum: field.aggregation.sum,
1943+ avg: field.aggregation.avg,
1944+ ),
1945+ ],
1946+ ),
1947+ );
1948+
1949+ final response = await firestore._client.v1 ((client) async {
1950+ return client.projects.databases.documents.runAggregationQuery (
1951+ aggregationQuery,
1952+ query._buildProtoParentPath (),
1953+ );
1954+ });
1955+
1956+ final results = < String , Object ? > {};
1957+ Timestamp ? readTime;
1958+
1959+ for (final result in response) {
1960+ if (result.result != null && result.result! .aggregateFields != null ) {
1961+ for (final entry in result.result! .aggregateFields! .entries) {
1962+ final value = entry.value;
1963+ if (value.integerValue != null ) {
1964+ results[entry.key] = int .parse (value.integerValue! );
1965+ } else if (value.doubleValue != null ) {
1966+ results[entry.key] = value.doubleValue;
1967+ } else if (value.nullValue != null ) {
1968+ results[entry.key] = null ;
1969+ }
1970+ }
1971+ }
1972+
1973+ if (result.readTime != null ) {
1974+ readTime = Timestamp ._fromString (result.readTime! );
1975+ }
1976+ }
1977+
1978+ return AggregateQuerySnapshot ._(
1979+ query: this ,
1980+ readTime: readTime,
1981+ data: results,
1982+ );
1983+ }
1984+
1985+ @override
1986+ bool operator == (Object other) {
1987+ return other is AggregateQuery &&
1988+ query == other.query &&
1989+ const ListEquality <_AggregateFieldInternal >()
1990+ .equals (aggregations, other.aggregations);
1991+ }
1992+
1993+ @override
1994+ int get hashCode => Object .hash (
1995+ query,
1996+ const ListEquality <_AggregateFieldInternal >().hash (aggregations),
1997+ );
1998+ }
1999+
2000+ /// The results of executing an aggregation query.
2001+ @immutable
2002+ class AggregateQuerySnapshot {
2003+ const AggregateQuerySnapshot ._({
2004+ required this .query,
2005+ required this .readTime,
2006+ required this .data,
2007+ });
2008+
2009+ /// The query that was executed to produce this result.
2010+ final AggregateQuery query;
2011+
2012+ /// The time this snapshot was obtained.
2013+ final Timestamp ? readTime;
2014+
2015+ /// The raw aggregation data, keyed by alias.
2016+ final Map <String , Object ?> data;
2017+
2018+ /// The count of documents that match the query. Returns `null` if the
2019+ /// count aggregation was not performed.
2020+ int ? get count => data['count' ] as int ? ;
2021+
2022+ /// Gets the sum for the specified field. Returns `null` if the
2023+ /// sum aggregation was not performed.
2024+ ///
2025+ /// - [field] : The field that was summed.
2026+ num ? getSum (String field) {
2027+ final alias = 'sum_$field ' ;
2028+ final value = data[alias];
2029+ if (value == null ) return null ;
2030+ if (value is int || value is double ) return value as num ;
2031+ // Handle case where sum might be returned as a string
2032+ if (value is String ) return num .tryParse (value);
2033+ return null ;
2034+ }
2035+
2036+ /// Gets the average for the specified field. Returns `null` if the
2037+ /// average aggregation was not performed.
2038+ ///
2039+ /// - [field] : The field that was averaged.
2040+ double ? getAverage (String field) {
2041+ final alias = 'avg_$field ' ;
2042+ final value = data[alias];
2043+ if (value == null ) return null ;
2044+ if (value is double ) return value;
2045+ if (value is int ) return value.toDouble ();
2046+ // Handle case where average might be returned as a string
2047+ if (value is String ) return double .tryParse (value);
2048+ return null ;
2049+ }
2050+
2051+ /// Gets an aggregate field by alias.
2052+ ///
2053+ /// - [alias] : The alias of the aggregate field to retrieve.
2054+ Object ? getField (String alias) => data[alias];
2055+
2056+ @override
2057+ bool operator == (Object other) {
2058+ return other is AggregateQuerySnapshot &&
2059+ query == other.query &&
2060+ readTime == other.readTime &&
2061+ const MapEquality <String , Object ?>().equals (data, other.data);
2062+ }
2063+
2064+ @override
2065+ int get hashCode => Object .hash (query, readTime, data);
16832066}
16842067
16852068/// A QuerySnapshot contains zero or more [QueryDocumentSnapshot] objects
0 commit comments