Skip to content

Commit 510c9b5

Browse files
Aggregation implementation and test improvements (#7170)
* Updating sum/avg tests. * Back-porting: return aggregation results as a map from the remote layer instead of as an ObjectValue. * Add long-alias support for aggregations.
1 parent 756db42 commit 510c9b5

File tree

11 files changed

+128
-108
lines changed

11 files changed

+128
-108
lines changed

.changeset/olive-goats-greet.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@firebase/firestore": patch
3+
---
4+
5+
Simplified the internal handling of aggregation results.

packages/firestore/src/api/aggregate.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ import { AggregateImpl } from '../core/aggregate';
2020
import { firestoreClientRunAggregateQuery } from '../core/firestore_client';
2121
import { count } from '../lite-api/aggregate';
2222
import { AggregateQuerySnapshot } from '../lite-api/aggregate_types';
23-
import { AggregateAlias } from '../model/aggregate_alias';
24-
import { ObjectValue } from '../model/object_value';
23+
import { ApiClientObjectMap, Value } from '../protos/firestore_proto_api';
2524
import { cast } from '../util/input_validation';
2625
import { mapToArray } from '../util/obj';
2726

@@ -110,7 +109,7 @@ export function getAggregateFromServer<T extends AggregateSpec>(
110109

111110
const internalAggregates = mapToArray(aggregateSpec, (aggregate, alias) => {
112111
return new AggregateImpl(
113-
new AggregateAlias(alias),
112+
alias,
114113
aggregate._aggregateType,
115114
aggregate._internalFieldPath
116115
);
@@ -136,7 +135,7 @@ export function getAggregateFromServer<T extends AggregateSpec>(
136135
function convertToAggregateQuerySnapshot<T extends AggregateSpec>(
137136
firestore: Firestore,
138137
query: Query<unknown>,
139-
aggregateResult: ObjectValue
138+
aggregateResult: ApiClientObjectMap<Value>
140139
): AggregateQuerySnapshot<T> {
141140
const userDataWriter = new ExpUserDataWriter(firestore);
142141
const querySnapshot = new AggregateQuerySnapshot<T>(

packages/firestore/src/core/aggregate.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { AggregateAlias } from '../model/aggregate_alias';
1918
import { FieldPath } from '../model/path';
2019

2120
/**
@@ -29,7 +28,7 @@ export type AggregateType = 'count' | 'avg' | 'sum';
2928
*/
3029
export interface Aggregate {
3130
readonly fieldPath?: FieldPath;
32-
readonly alias: AggregateAlias;
31+
readonly alias: string;
3332
readonly aggregateType: AggregateType;
3433
}
3534

@@ -38,7 +37,7 @@ export interface Aggregate {
3837
*/
3938
export class AggregateImpl implements Aggregate {
4039
constructor(
41-
readonly alias: AggregateAlias,
40+
readonly alias: string,
4241
readonly aggregateType: AggregateType,
4342
readonly fieldPath?: FieldPath
4443
) {}

packages/firestore/src/core/firestore_client.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,10 @@ import { Document } from '../model/document';
3636
import { DocumentKey } from '../model/document_key';
3737
import { FieldIndex } from '../model/field_index';
3838
import { Mutation } from '../model/mutation';
39-
import { ObjectValue } from '../model/object_value';
4039
import { toByteStreamReader } from '../platform/byte_stream_reader';
4140
import { newSerializer } from '../platform/serializer';
4241
import { newTextEncoder } from '../platform/text_serializer';
42+
import { ApiClientObjectMap, Value } from '../protos/firestore_proto_api';
4343
import { Datastore, invokeRunAggregationQueryRpc } from '../remote/datastore';
4444
import {
4545
RemoteStore,
@@ -526,8 +526,8 @@ export function firestoreClientRunAggregateQuery(
526526
client: FirestoreClient,
527527
query: Query,
528528
aggregates: Aggregate[]
529-
): Promise<ObjectValue> {
530-
const deferred = new Deferred<ObjectValue>();
529+
): Promise<ApiClientObjectMap<Value>> {
530+
const deferred = new Deferred<ApiClientObjectMap<Value>>();
531531

532532
client.asyncQueue.enqueueAndForget(async () => {
533533
// TODO (sum/avg) should we update this to use the event manager?

packages/firestore/src/lite-api/aggregate.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@
1818
import { deepEqual } from '@firebase/util';
1919

2020
import { AggregateImpl } from '../core/aggregate';
21-
import { AggregateAlias } from '../model/aggregate_alias';
22-
import { ObjectValue } from '../model/object_value';
21+
import { ApiClientObjectMap, Value } from '../protos/firestore_proto_api';
2322
import { invokeRunAggregationQueryRpc } from '../remote/datastore';
2423
import { cast } from '../util/input_validation';
2524
import { mapToArray } from '../util/obj';
@@ -96,7 +95,7 @@ export function getAggregate<T extends AggregateSpec>(
9695

9796
const internalAggregates = mapToArray(aggregateSpec, (aggregate, alias) => {
9897
return new AggregateImpl(
99-
new AggregateAlias(alias),
98+
alias,
10099
aggregate._aggregateType,
101100
aggregate._internalFieldPath
102101
);
@@ -115,7 +114,7 @@ export function getAggregate<T extends AggregateSpec>(
115114
function convertToAggregateQuerySnapshot<T extends AggregateSpec>(
116115
firestore: Firestore,
117116
query: Query<unknown>,
118-
aggregateResult: ObjectValue
117+
aggregateResult: ApiClientObjectMap<Value>
119118
): AggregateQuerySnapshot<T> {
120119
const userDataWriter = new LiteUserDataWriter(firestore);
121120
const querySnapshot = new AggregateQuerySnapshot<T>(

packages/firestore/src/lite-api/aggregate_types.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
*/
1717

1818
import { AggregateType } from '../core/aggregate';
19-
import { ObjectValue } from '../model/object_value';
2019
import { FieldPath as InternalFieldPath } from '../model/path';
20+
import { ApiClientObjectMap, Value } from '../protos/firestore_proto_api';
2121

2222
import { Query } from './reference';
2323
import { AbstractUserDataWriter } from './user_data_writer';
@@ -85,7 +85,7 @@ export class AggregateQuerySnapshot<T extends AggregateSpec> {
8585
constructor(
8686
query: Query<unknown>,
8787
private readonly _userDataWriter: AbstractUserDataWriter,
88-
private readonly _data: ObjectValue
88+
private readonly _data: ApiClientObjectMap<Value>
8989
) {
9090
this.query = query;
9191
}
@@ -102,8 +102,8 @@ export class AggregateQuerySnapshot<T extends AggregateSpec> {
102102
* query.
103103
*/
104104
data(): AggregateSpecData<T> {
105-
return this._userDataWriter.convertValue(
106-
this._data.value
105+
return this._userDataWriter.convertObjectMap(
106+
this._data
107107
) as AggregateSpecData<T>;
108108
}
109109
}

packages/firestore/src/lite-api/user_data_writer.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,12 @@ import {
3232
import { TypeOrder } from '../model/type_order';
3333
import { typeOrder } from '../model/values';
3434
import {
35+
ApiClientObjectMap,
3536
ArrayValue as ProtoArrayValue,
3637
LatLng as ProtoLatLng,
3738
MapValue as ProtoMapValue,
3839
Timestamp as ProtoTimestamp,
40+
Value,
3941
Value as ProtoValue
4042
} from '../protos/firestore_proto_api';
4143
import { isValidResourceName } from '../remote/serializer';
@@ -91,9 +93,19 @@ export abstract class AbstractUserDataWriter {
9193
private convertObject(
9294
mapValue: ProtoMapValue,
9395
serverTimestampBehavior: ServerTimestampBehavior
96+
): DocumentData {
97+
return this.convertObjectMap(mapValue.fields, serverTimestampBehavior);
98+
}
99+
100+
/**
101+
* @internal
102+
*/
103+
convertObjectMap(
104+
fields: ApiClientObjectMap<Value> | undefined,
105+
serverTimestampBehavior: ServerTimestampBehavior = 'none'
94106
): DocumentData {
95107
const result: DocumentData = {};
96-
forEach(mapValue.fields, (key, value) => {
108+
forEach(fields, (key, value) => {
97109
result[key] = this.convertValue(value, serverTimestampBehavior);
98110
});
99111
return result;

packages/firestore/src/model/aggregate_alias.ts

-48
This file was deleted.

packages/firestore/src/remote/datastore.ts

+31-7
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,20 @@ import { Query, queryToTarget } from '../core/query';
2222
import { Document } from '../model/document';
2323
import { DocumentKey } from '../model/document_key';
2424
import { Mutation } from '../model/mutation';
25-
import { ObjectValue } from '../model/object_value';
2625
import {
26+
ApiClientObjectMap,
2727
BatchGetDocumentsRequest as ProtoBatchGetDocumentsRequest,
2828
BatchGetDocumentsResponse as ProtoBatchGetDocumentsResponse,
2929
RunAggregationQueryRequest as ProtoRunAggregationQueryRequest,
3030
RunAggregationQueryResponse as ProtoRunAggregationQueryResponse,
3131
RunQueryRequest as ProtoRunQueryRequest,
32-
RunQueryResponse as ProtoRunQueryResponse
32+
RunQueryResponse as ProtoRunQueryResponse,
33+
Value
3334
} from '../protos/firestore_proto_api';
3435
import { debugAssert, debugCast, hardAssert } from '../util/assert';
3536
import { AsyncQueue } from '../util/async_queue';
3637
import { Code, FirestoreError } from '../util/error';
38+
import { isNullOrUndefined } from '../util/types';
3739

3840
import { Connection } from './connection';
3941
import {
@@ -50,8 +52,7 @@ import {
5052
toMutation,
5153
toName,
5254
toQueryTarget,
53-
toRunAggregationQueryRequest,
54-
fromAggregationResult
55+
toRunAggregationQueryRequest
5556
} from './serializer';
5657

5758
/**
@@ -243,9 +244,9 @@ export async function invokeRunAggregationQueryRpc(
243244
datastore: Datastore,
244245
query: Query,
245246
aggregates: Aggregate[]
246-
): Promise<ObjectValue> {
247+
): Promise<ApiClientObjectMap<Value>> {
247248
const datastoreImpl = debugCast(datastore, DatastoreImpl);
248-
const request = toRunAggregationQueryRequest(
249+
const { request, aliasMap } = toRunAggregationQueryRequest(
249250
datastoreImpl.serializer,
250251
queryToTarget(query),
251252
aggregates
@@ -267,8 +268,31 @@ export async function invokeRunAggregationQueryRpc(
267268
filteredResult.length === 1,
268269
'Aggregation fields are missing from result.'
269270
);
271+
debugAssert(
272+
!isNullOrUndefined(filteredResult[0].result),
273+
'aggregationQueryResponse.result'
274+
);
275+
debugAssert(
276+
!isNullOrUndefined(filteredResult[0].result.aggregateFields),
277+
'aggregationQueryResponse.result.aggregateFields'
278+
);
279+
280+
// Remap the short-form aliases that were sent to the server
281+
// to the client-side aliases. Users will access the results
282+
// using the client-side alias.
283+
const unmappedAggregateFields = filteredResult[0].result?.aggregateFields;
284+
const remappedFields = Object.keys(unmappedAggregateFields).reduce<
285+
ApiClientObjectMap<Value>
286+
>((accumulator, key) => {
287+
debugAssert(
288+
!isNullOrUndefined(aliasMap[key]),
289+
`'${key}' not present in aliasMap result`
290+
);
291+
accumulator[aliasMap[key]] = unmappedAggregateFields[key]!;
292+
return accumulator;
293+
}, {});
270294

271-
return fromAggregationResult(filteredResult[0]);
295+
return remappedFields;
272296
}
273297

274298
export function newPersistentWriteStream(

packages/firestore/src/remote/serializer.ts

+23-25
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ import {
8181
Precondition as ProtoPrecondition,
8282
QueryTarget as ProtoQueryTarget,
8383
RunAggregationQueryRequest as ProtoRunAggregationQueryRequest,
84-
RunAggregationQueryResponse as ProtoRunAggregationQueryResponse,
8584
Aggregation as ProtoAggregation,
8685
Status as ProtoStatus,
8786
Target as ProtoTarget,
@@ -438,22 +437,6 @@ export function fromDocument(
438437
return hasCommittedMutations ? result.setHasCommittedMutations() : result;
439438
}
440439

441-
export function fromAggregationResult(
442-
aggregationQueryResponse: ProtoRunAggregationQueryResponse
443-
): ObjectValue {
444-
assertPresent(
445-
aggregationQueryResponse.result,
446-
'aggregationQueryResponse.result'
447-
);
448-
assertPresent(
449-
aggregationQueryResponse.result.aggregateFields,
450-
'aggregationQueryResponse.result.aggregateFields'
451-
);
452-
return new ObjectValue({
453-
mapValue: { fields: aggregationQueryResponse.result?.aggregateFields }
454-
});
455-
}
456-
457440
function fromFound(
458441
serializer: JsonProtoSerializer,
459442
doc: ProtoBatchGetDocumentsResponse
@@ -908,26 +891,38 @@ export function toRunAggregationQueryRequest(
908891
serializer: JsonProtoSerializer,
909892
target: Target,
910893
aggregates: Aggregate[]
911-
): ProtoRunAggregationQueryRequest {
894+
): {
895+
request: ProtoRunAggregationQueryRequest;
896+
aliasMap: Record<string, string>;
897+
} {
912898
const queryTarget = toQueryTarget(serializer, target);
899+
const aliasMap: Record<string, string> = {};
913900

914901
const aggregations: ProtoAggregation[] = [];
902+
let aggregationNum = 0;
903+
915904
aggregates.forEach(aggregate => {
905+
// Map all client-side aliases to a unique short-form
906+
// alias. This avoids issues with client-side aliases that
907+
// exceed the 1500-byte string size limit.
908+
const serverAlias = `aggregate_${aggregationNum++}`;
909+
aliasMap[serverAlias] = aggregate.alias;
910+
916911
if (aggregate.aggregateType === 'count') {
917912
aggregations.push({
918-
alias: aggregate.alias.canonicalString(),
913+
alias: serverAlias,
919914
count: {}
920915
});
921916
} else if (aggregate.aggregateType === 'avg') {
922917
aggregations.push({
923-
alias: aggregate.alias.canonicalString(),
918+
alias: serverAlias,
924919
avg: {
925920
field: toFieldPathReference(aggregate.fieldPath!)
926921
}
927922
});
928923
} else if (aggregate.aggregateType === 'sum') {
929924
aggregations.push({
930-
alias: aggregate.alias.canonicalString(),
925+
alias: serverAlias,
931926
sum: {
932927
field: toFieldPathReference(aggregate.fieldPath!)
933928
}
@@ -936,11 +931,14 @@ export function toRunAggregationQueryRequest(
936931
});
937932

938933
return {
939-
structuredAggregationQuery: {
940-
aggregations,
941-
structuredQuery: queryTarget.structuredQuery
934+
request: {
935+
structuredAggregationQuery: {
936+
aggregations,
937+
structuredQuery: queryTarget.structuredQuery
938+
},
939+
parent: queryTarget.parent
942940
},
943-
parent: queryTarget.parent
941+
aliasMap
944942
};
945943
}
946944

0 commit comments

Comments
 (0)