Skip to content

Commit 67c5a0d

Browse files
Sum and average (#6952)
Refactoring aggregations in Firestore to support sum and average. Sum and average are not public. --------- Co-authored-by: MarkDuckworth <[email protected]>
1 parent ce2671a commit 67c5a0d

22 files changed

+2441
-161
lines changed

.changeset/popular-items-hide.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@firebase/firestore": patch
3+
---
4+
5+
Refactoring the aggregation implementation to support future aggregate functions.

common/api-review/firestore-lite.api.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ export type AddPrefixToKeys<Prefix extends string, T extends Record<string, unkn
1919

2020
// @public
2121
export class AggregateField<T> {
22-
type: string;
22+
readonly type = "AggregateField";
2323
}
2424

2525
// @public
26-
export type AggregateFieldType = AggregateField<number>;
26+
export type AggregateFieldType = AggregateField<number | null>;
2727

2828
// @public
2929
export class AggregateQuerySnapshot<T extends AggregateSpec> {

common/api-review/firestore.api.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ export type AddPrefixToKeys<Prefix extends string, T extends Record<string, unkn
1919

2020
// @public
2121
export class AggregateField<T> {
22-
type: string;
22+
readonly type = "AggregateField";
2323
}
2424

2525
// @public
26-
export type AggregateFieldType = AggregateField<number>;
26+
export type AggregateFieldType = AggregateField<number | null>;
2727

2828
// @public
2929
export class AggregateQuerySnapshot<T extends AggregateSpec> {

packages/firestore/lite/index.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,21 @@ registerFirestore();
2929

3030
export {
3131
aggregateQuerySnapshotEqual,
32-
getCount
32+
getCount,
33+
getAggregate,
34+
count,
35+
sum,
36+
average,
37+
aggregateFieldEqual
3338
} from '../src/lite-api/aggregate';
3439

3540
export {
3641
AggregateField,
3742
AggregateFieldType,
3843
AggregateSpec,
3944
AggregateSpecData,
40-
AggregateQuerySnapshot
45+
AggregateQuerySnapshot,
46+
AggregateType
4147
} from '../src/lite-api/aggregate_types';
4248

4349
export { FirestoreSettings as Settings } from '../src/lite-api/settings';

packages/firestore/src/api.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,21 @@
1717

1818
export {
1919
aggregateQuerySnapshotEqual,
20-
getCountFromServer
20+
getCountFromServer,
21+
getAggregateFromServer,
22+
count,
23+
sum,
24+
average,
25+
aggregateFieldEqual
2126
} from './api/aggregate';
2227

2328
export {
2429
AggregateField,
2530
AggregateFieldType,
2631
AggregateSpec,
2732
AggregateSpecData,
28-
AggregateQuerySnapshot
33+
AggregateQuerySnapshot,
34+
AggregateType
2935
} from './lite-api/aggregate_types';
3036

3137
export { FieldPath, documentId } from './api/field_path';

packages/firestore/src/api/aggregate.ts

+97-8
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,26 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { Query } from '../api';
19-
import { firestoreClientRunCountQuery } from '../core/firestore_client';
20-
import {
21-
AggregateField,
22-
AggregateQuerySnapshot
23-
} from '../lite-api/aggregate_types';
18+
import { AggregateField, AggregateSpec, Query } from '../api';
19+
import { AggregateImpl } from '../core/aggregate';
20+
import { firestoreClientRunAggregateQuery } from '../core/firestore_client';
21+
import { count } from '../lite-api/aggregate';
22+
import { AggregateQuerySnapshot } from '../lite-api/aggregate_types';
23+
import { AggregateAlias } from '../model/aggregate_alias';
24+
import { ObjectValue } from '../model/object_value';
2425
import { cast } from '../util/input_validation';
26+
import { mapToArray } from '../util/obj';
2527

2628
import { ensureFirestoreConfigured, Firestore } from './database';
2729
import { ExpUserDataWriter } from './reference_impl';
2830

29-
export { aggregateQuerySnapshotEqual } from '../lite-api/aggregate';
31+
export {
32+
aggregateQuerySnapshotEqual,
33+
count,
34+
sum,
35+
average,
36+
aggregateFieldEqual
37+
} from '../lite-api/aggregate';
3038

3139
/**
3240
* Calculates the number of documents in the result set of the given query,
@@ -52,8 +60,89 @@ export { aggregateQuerySnapshotEqual } from '../lite-api/aggregate';
5260
export function getCountFromServer(
5361
query: Query<unknown>
5462
): Promise<AggregateQuerySnapshot<{ count: AggregateField<number> }>> {
63+
const countQuerySpec: { count: AggregateField<number> } = {
64+
count: count()
65+
};
66+
67+
return getAggregateFromServer(query, countQuerySpec);
68+
}
69+
70+
/**
71+
* Calculates the specified aggregations over the documents in the result
72+
* set of the given query, without actually downloading the documents.
73+
*
74+
* Using this function to perform aggregations is efficient because only the
75+
* final aggregation values, not the documents' data, is downloaded. This
76+
* function can even perform aggregations of the documents if the result set
77+
* would be prohibitively large to download entirely (e.g. thousands of documents).
78+
*
79+
* The result received from the server is presented, unaltered, without
80+
* considering any local state. That is, documents in the local cache are not
81+
* taken into consideration, neither are local modifications not yet
82+
* synchronized with the server. Previously-downloaded results, if any, are not
83+
* used: every request using this source necessarily involves a round trip to
84+
* the server.
85+
*
86+
* @param query The query whose result set to aggregate over.
87+
* @param aggregateSpec An `AggregateSpec` object that specifies the aggregates
88+
* to perform over the result set. The AggregateSpec specifies aliases for each
89+
* aggregate, which can be used to retrieve the aggregate result.
90+
* @example
91+
* ```typescript
92+
* const aggregateSnapshot = await getAggregateFromServer(query, {
93+
* countOfDocs: count(),
94+
* totalHours: sum('hours'),
95+
* averageScore: average('score')
96+
* });
97+
*
98+
* const countOfDocs: number = aggregateSnapshot.data().countOfDocs;
99+
* const totalHours: number = aggregateSnapshot.data().totalHours;
100+
* const averageScore: number | null = aggregateSnapshot.data().averageScore;
101+
* ```
102+
* @internal TODO (sum/avg) remove when public
103+
*/
104+
export function getAggregateFromServer<T extends AggregateSpec>(
105+
query: Query<unknown>,
106+
aggregateSpec: T
107+
): Promise<AggregateQuerySnapshot<T>> {
55108
const firestore = cast(query.firestore, Firestore);
56109
const client = ensureFirestoreConfigured(firestore);
110+
111+
const internalAggregates = mapToArray(aggregateSpec, (aggregate, alias) => {
112+
return new AggregateImpl(
113+
new AggregateAlias(alias),
114+
aggregate._aggregateType,
115+
aggregate._internalFieldPath
116+
);
117+
});
118+
119+
// Run the aggregation and convert the results
120+
return firestoreClientRunAggregateQuery(
121+
client,
122+
query._query,
123+
internalAggregates
124+
).then(aggregateResult =>
125+
convertToAggregateQuerySnapshot(firestore, query, aggregateResult)
126+
);
127+
}
128+
129+
/**
130+
* Converts the core aggregration result to an `AggregateQuerySnapshot`
131+
* that can be returned to the consumer.
132+
* @param query
133+
* @param aggregateResult Core aggregation result
134+
* @internal
135+
*/
136+
function convertToAggregateQuerySnapshot<T extends AggregateSpec>(
137+
firestore: Firestore,
138+
query: Query<unknown>,
139+
aggregateResult: ObjectValue
140+
): AggregateQuerySnapshot<T> {
57141
const userDataWriter = new ExpUserDataWriter(firestore);
58-
return firestoreClientRunCountQuery(client, query, userDataWriter);
142+
const querySnapshot = new AggregateQuerySnapshot<T>(
143+
query,
144+
userDataWriter,
145+
aggregateResult
146+
);
147+
return querySnapshot;
59148
}
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* @license
3+
* Copyright 2023 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { AggregateAlias } from '../model/aggregate_alias';
19+
import { FieldPath } from '../model/path';
20+
21+
/**
22+
* Union type representing the aggregate type to be performed.
23+
* @internal
24+
*/
25+
export type AggregateType = 'count' | 'avg' | 'sum';
26+
27+
/**
28+
* Represents an Aggregate to be performed over a query result set.
29+
*/
30+
export interface Aggregate {
31+
readonly fieldPath?: FieldPath;
32+
readonly alias: AggregateAlias;
33+
readonly aggregateType: AggregateType;
34+
}
35+
36+
/**
37+
* Concrete implementation of the Aggregate type.
38+
*/
39+
export class AggregateImpl implements Aggregate {
40+
constructor(
41+
readonly alias: AggregateAlias,
42+
readonly aggregateType: AggregateType,
43+
readonly fieldPath?: FieldPath
44+
) {}
45+
}

packages/firestore/src/core/count_query_runner.ts

-70
This file was deleted.

0 commit comments

Comments
 (0)