Skip to content

Commit 457265f

Browse files
committed
Fixing memoization issue with queryToAggregateTarget.
1 parent 513aa21 commit 457265f

File tree

3 files changed

+111
-50
lines changed

3 files changed

+111
-50
lines changed

packages/firestore/src/core/query.ts

Lines changed: 57 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -269,62 +269,71 @@ export function queryNormalizedOrderBy(query: Query): OrderBy[] {
269269
* Converts this `Query` instance to it's corresponding `Target` representation.
270270
*/
271271
export function queryToTarget(query: Query): Target {
272-
return _queryToTarget(query, queryNormalizedOrderBy(query));
272+
const queryImpl = debugCast(query, QueryImpl);
273+
if (!queryImpl.memoizedTarget) {
274+
queryImpl.memoizedTarget = _queryToTarget(
275+
queryImpl,
276+
queryNormalizedOrderBy(query)
277+
);
278+
}
279+
280+
return queryImpl.memoizedTarget;
273281
}
274282

275283
/**
276284
* Converts this `Query` instance to it's corresponding `Target` representation,
277285
* for use within an aggregate query.
278286
*/
279-
export function aggregateQueryToTarget(query: Query): Target {
280-
// Do not include implicit order-bys for aggregate queries.
281-
return _queryToTarget(query, query.explicitOrderBy);
282-
}
283-
284-
export function _queryToTarget(query: Query, orderBys: OrderBy[]): Target {
287+
export function queryToAggregateTarget(query: Query): Target {
285288
const queryImpl = debugCast(query, QueryImpl);
286-
if (!queryImpl.memoizedTarget) {
287-
if (queryImpl.limitType === LimitType.First) {
288-
queryImpl.memoizedTarget = newTarget(
289-
queryImpl.path,
290-
queryImpl.collectionGroup,
291-
orderBys,
292-
queryImpl.filters,
293-
queryImpl.limit,
294-
queryImpl.startAt,
295-
queryImpl.endAt
296-
);
297-
} else {
298-
// Flip the orderBy directions since we want the last results
299-
orderBys = orderBys.map(orderBy => {
300-
const dir =
301-
orderBy.dir === Direction.DESCENDING
302-
? Direction.ASCENDING
303-
: Direction.DESCENDING;
304-
return new OrderBy(orderBy.field, dir);
305-
});
306-
307-
// We need to swap the cursors to match the now-flipped query ordering.
308-
const startAt = queryImpl.endAt
309-
? new Bound(queryImpl.endAt.position, queryImpl.endAt.inclusive)
310-
: null;
311-
const endAt = queryImpl.startAt
312-
? new Bound(queryImpl.startAt.position, queryImpl.startAt.inclusive)
313-
: null;
314-
315-
// Now return as a LimitType.First query.
316-
queryImpl.memoizedTarget = newTarget(
317-
queryImpl.path,
318-
queryImpl.collectionGroup,
319-
orderBys,
320-
queryImpl.filters,
321-
queryImpl.limit,
322-
startAt,
323-
endAt
324-
);
325-
}
289+
290+
// Do not include implicit order-bys for aggregate queries.
291+
return _queryToTarget(queryImpl, query.explicitOrderBy);
292+
}
293+
294+
export function _queryToTarget(
295+
queryImpl: QueryImpl,
296+
orderBys: OrderBy[]
297+
): Target {
298+
if (queryImpl.limitType === LimitType.First) {
299+
return newTarget(
300+
queryImpl.path,
301+
queryImpl.collectionGroup,
302+
orderBys,
303+
queryImpl.filters,
304+
queryImpl.limit,
305+
queryImpl.startAt,
306+
queryImpl.endAt
307+
);
308+
} else {
309+
// Flip the orderBy directions since we want the last results
310+
orderBys = orderBys.map(orderBy => {
311+
const dir =
312+
orderBy.dir === Direction.DESCENDING
313+
? Direction.ASCENDING
314+
: Direction.DESCENDING;
315+
return new OrderBy(orderBy.field, dir);
316+
});
317+
318+
// We need to swap the cursors to match the now-flipped query ordering.
319+
const startAt = queryImpl.endAt
320+
? new Bound(queryImpl.endAt.position, queryImpl.endAt.inclusive)
321+
: null;
322+
const endAt = queryImpl.startAt
323+
? new Bound(queryImpl.startAt.position, queryImpl.startAt.inclusive)
324+
: null;
325+
326+
// Now return as a LimitType.First query.
327+
return newTarget(
328+
queryImpl.path,
329+
queryImpl.collectionGroup,
330+
orderBys,
331+
queryImpl.filters,
332+
queryImpl.limit,
333+
startAt,
334+
endAt
335+
);
326336
}
327-
return queryImpl.memoizedTarget!;
328337
}
329338

330339
export function queryWithAddedFilter(query: Query, filter: Filter): Query {

packages/firestore/src/remote/datastore.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import { CredentialsProvider } from '../api/credentials';
1919
import { User } from '../auth/user';
2020
import { Aggregate } from '../core/aggregate';
21-
import { aggregateQueryToTarget, Query, queryToTarget } from '../core/query';
21+
import { queryToAggregateTarget, Query, queryToTarget } from '../core/query';
2222
import { Document } from '../model/document';
2323
import { DocumentKey } from '../model/document_key';
2424
import { Mutation } from '../model/mutation';
@@ -248,7 +248,7 @@ export async function invokeRunAggregationQueryRpc(
248248
const datastoreImpl = debugCast(datastore, DatastoreImpl);
249249
const { request, aliasMap } = toRunAggregationQueryRequest(
250250
datastoreImpl.serializer,
251-
aggregateQueryToTarget(query),
251+
queryToAggregateTarget(query),
252252
aggregates
253253
);
254254

packages/firestore/test/unit/core/query.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
queryWithLimit,
3434
queryWithStartAt,
3535
stringifyQuery,
36+
queryToAggregateTarget,
3637
queryToTarget,
3738
QueryImpl,
3839
queryEquals,
@@ -852,6 +853,57 @@ describe('Query', () => {
852853
assertQueryMatches(query5, [doc3], [doc1, doc2, doc4, doc5]);
853854
});
854855

856+
it('generates appropriate order-bys for aggregate and non-aggregate targets', () => {
857+
const col = newQueryForPath(ResourcePath.fromString('collection'));
858+
859+
// Build two identical queries
860+
const query1 = queryWithAddedFilter(col, filter('foo', '>', 1));
861+
const query2 = queryWithAddedFilter(col, filter('foo', '>', 1));
862+
863+
// Compute an aggregate and non-aggregate target from the queries
864+
const aggregateTarget = queryToAggregateTarget(query1);
865+
const target = queryToTarget(query2);
866+
867+
expect(aggregateTarget.orderBy.length).to.equal(0);
868+
expect(target.orderBy.length).to.equal(2);
869+
expect(target.orderBy[0].dir).to.equal('asc');
870+
expect(target.orderBy[0].field.canonicalString()).to.equal('foo');
871+
expect(target.orderBy[1].dir).to.equal('asc');
872+
expect(target.orderBy[1].field.canonicalString()).to.equal('__name__');
873+
});
874+
875+
it('generated order-bys are not affected by previously memoized targets', () => {
876+
const col = newQueryForPath(ResourcePath.fromString('collection'));
877+
878+
// Build two identical queries
879+
const query1 = queryWithAddedFilter(col, filter('foo', '>', 1));
880+
const query2 = queryWithAddedFilter(col, filter('foo', '>', 1));
881+
882+
// query1 - first to aggregate target, then to non-aggregate target
883+
const aggregateTarget1 = queryToAggregateTarget(query1);
884+
const target1 = queryToTarget(query1);
885+
886+
// query2 - first to non-aggregate target, then to aggregate target
887+
const target2 = queryToTarget(query2);
888+
const aggregateTarget2 = queryToAggregateTarget(query2);
889+
890+
expect(aggregateTarget1.orderBy.length).to.equal(0);
891+
892+
expect(aggregateTarget2.orderBy.length).to.equal(0);
893+
894+
expect(target1.orderBy.length).to.equal(2);
895+
expect(target1.orderBy[0].dir).to.equal('asc');
896+
expect(target1.orderBy[0].field.canonicalString()).to.equal('foo');
897+
expect(target1.orderBy[1].dir).to.equal('asc');
898+
expect(target1.orderBy[1].field.canonicalString()).to.equal('__name__');
899+
900+
expect(target2.orderBy.length).to.equal(2);
901+
expect(target2.orderBy[0].dir).to.equal('asc');
902+
expect(target2.orderBy[0].field.canonicalString()).to.equal('foo');
903+
expect(target2.orderBy[1].dir).to.equal('asc');
904+
expect(target2.orderBy[1].field.canonicalString()).to.equal('__name__');
905+
});
906+
855907
function assertQueryMatches(
856908
query: Query,
857909
matching: MutableDocument[],

0 commit comments

Comments
 (0)