@@ -43,7 +43,7 @@ export const enum LimitType {
4343/**
4444 * The Query interface defines all external properties of a query.
4545 *
46- * QueryImpl implements this interface to provide memoization for `queryOrderBy `
46+ * QueryImpl implements this interface to provide memoization for `queryNormalizedOrderBy `
4747 * and `queryToTarget`.
4848 */
4949export interface Query {
@@ -65,11 +65,18 @@ export interface Query {
6565 * Visible for testing.
6666 */
6767export class QueryImpl implements Query {
68- memoizedOrderBy : OrderBy [ ] | null = null ;
68+ memoizedNormalizedOrderBy : OrderBy [ ] | null = null ;
6969
70- // The corresponding `Target` of this `Query` instance.
70+ // The corresponding `Target` of this `Query` instance, for use with
71+ // non-aggregate queries.
7172 memoizedTarget : Target | null = null ;
7273
74+ // The corresponding `Target` of this `Query` instance, for use with
75+ // aggregate queries. Unlike targets for non-aggregate queries,
76+ // aggregate query targets do not contain normalized order-bys, they only
77+ // contain explicit order-bys.
78+ memoizedAggregateTarget : Target | null = null ;
79+
7380 /**
7481 * Initializes a Query with a path and optional additional query constraints.
7582 * Path must currently be empty if this is a collection group query.
@@ -86,13 +93,13 @@ export class QueryImpl implements Query {
8693 ) {
8794 if ( this . startAt ) {
8895 debugAssert (
89- this . startAt . position . length <= queryOrderBy ( this ) . length ,
96+ this . startAt . position . length <= queryNormalizedOrderBy ( this ) . length ,
9097 'Bound is longer than orderBy'
9198 ) ;
9299 }
93100 if ( this . endAt ) {
94101 debugAssert (
95- this . endAt . position . length <= queryOrderBy ( this ) . length ,
102+ this . endAt . position . length <= queryNormalizedOrderBy ( this ) . length ,
96103 'Bound is longer than orderBy'
97104 ) ;
98105 }
@@ -211,14 +218,16 @@ export function isCollectionGroupQuery(query: Query): boolean {
211218}
212219
213220/**
214- * Returns the implicit order by constraint that is used to execute the Query,
215- * which can be different from the order by constraints the user provided (e.g.
216- * the SDK and backend always orders by `__name__`).
221+ * Returns the normalized order-by constraint that is used to execute the Query,
222+ * which can be different from the order-by constraints the user provided (e.g.
223+ * the SDK and backend always orders by `__name__`). The normalized order-by
224+ * includes implicit order-bys in addition to the explicit user provided
225+ * order-bys.
217226 */
218- export function queryOrderBy ( query : Query ) : OrderBy [ ] {
227+ export function queryNormalizedOrderBy ( query : Query ) : OrderBy [ ] {
219228 const queryImpl = debugCast ( query , QueryImpl ) ;
220- if ( queryImpl . memoizedOrderBy === null ) {
221- queryImpl . memoizedOrderBy = [ ] ;
229+ if ( queryImpl . memoizedNormalizedOrderBy === null ) {
230+ queryImpl . memoizedNormalizedOrderBy = [ ] ;
222231
223232 const inequalityField = getInequalityFilterField ( queryImpl ) ;
224233 const firstOrderByField = getFirstOrderByField ( queryImpl ) ;
@@ -227,9 +236,9 @@ export function queryOrderBy(query: Query): OrderBy[] {
227236 // inequality filter field for it to be a valid query.
228237 // Note that the default inequality field and key ordering is ascending.
229238 if ( ! inequalityField . isKeyField ( ) ) {
230- queryImpl . memoizedOrderBy . push ( new OrderBy ( inequalityField ) ) ;
239+ queryImpl . memoizedNormalizedOrderBy . push ( new OrderBy ( inequalityField ) ) ;
231240 }
232- queryImpl . memoizedOrderBy . push (
241+ queryImpl . memoizedNormalizedOrderBy . push (
233242 new OrderBy ( FieldPath . keyField ( ) , Direction . ASCENDING )
234243 ) ;
235244 } else {
@@ -241,76 +250,103 @@ export function queryOrderBy(query: Query): OrderBy[] {
241250 ) ;
242251 let foundKeyOrdering = false ;
243252 for ( const orderBy of queryImpl . explicitOrderBy ) {
244- queryImpl . memoizedOrderBy . push ( orderBy ) ;
253+ queryImpl . memoizedNormalizedOrderBy . push ( orderBy ) ;
245254 if ( orderBy . field . isKeyField ( ) ) {
246255 foundKeyOrdering = true ;
247256 }
248257 }
249258 if ( ! foundKeyOrdering ) {
250259 // The order of the implicit key ordering always matches the last
251- // explicit order by
260+ // explicit order- by
252261 const lastDirection =
253262 queryImpl . explicitOrderBy . length > 0
254263 ? queryImpl . explicitOrderBy [ queryImpl . explicitOrderBy . length - 1 ]
255264 . dir
256265 : Direction . ASCENDING ;
257- queryImpl . memoizedOrderBy . push (
266+ queryImpl . memoizedNormalizedOrderBy . push (
258267 new OrderBy ( FieldPath . keyField ( ) , lastDirection )
259268 ) ;
260269 }
261270 }
262271 }
263- return queryImpl . memoizedOrderBy ;
272+ return queryImpl . memoizedNormalizedOrderBy ;
264273}
265274
266275/**
267- * Converts this `Query` instance to it's corresponding `Target` representation.
276+ * Converts this `Query` instance to its corresponding `Target` representation.
268277 */
269278export function queryToTarget ( query : Query ) : Target {
270279 const queryImpl = debugCast ( query , QueryImpl ) ;
271280 if ( ! queryImpl . memoizedTarget ) {
272- if ( queryImpl . limitType === LimitType . First ) {
273- queryImpl . memoizedTarget = newTarget (
274- queryImpl . path ,
275- queryImpl . collectionGroup ,
276- queryOrderBy ( queryImpl ) ,
277- queryImpl . filters ,
278- queryImpl . limit ,
279- queryImpl . startAt ,
280- queryImpl . endAt
281- ) ;
282- } else {
283- // Flip the orderBy directions since we want the last results
284- const orderBys = [ ] as OrderBy [ ] ;
285- for ( const orderBy of queryOrderBy ( queryImpl ) ) {
286- const dir =
287- orderBy . dir === Direction . DESCENDING
288- ? Direction . ASCENDING
289- : Direction . DESCENDING ;
290- orderBys . push ( new OrderBy ( orderBy . field , dir ) ) ;
291- }
281+ queryImpl . memoizedTarget = _queryToTarget (
282+ queryImpl ,
283+ queryNormalizedOrderBy ( query )
284+ ) ;
285+ }
292286
293- // We need to swap the cursors to match the now-flipped query ordering.
294- const startAt = queryImpl . endAt
295- ? new Bound ( queryImpl . endAt . position , queryImpl . endAt . inclusive )
296- : null ;
297- const endAt = queryImpl . startAt
298- ? new Bound ( queryImpl . startAt . position , queryImpl . startAt . inclusive )
299- : null ;
300-
301- // Now return as a LimitType.First query.
302- queryImpl . memoizedTarget = newTarget (
303- queryImpl . path ,
304- queryImpl . collectionGroup ,
305- orderBys ,
306- queryImpl . filters ,
307- queryImpl . limit ,
308- startAt ,
309- endAt
310- ) ;
311- }
287+ return queryImpl . memoizedTarget ;
288+ }
289+
290+ /**
291+ * Converts this `Query` instance to its corresponding `Target` representation,
292+ * for use within an aggregate query. Unlike targets for non-aggregate queries,
293+ * aggregate query targets do not contain normalized order-bys, they only
294+ * contain explicit order-bys.
295+ */
296+ export function queryToAggregateTarget ( query : Query ) : Target {
297+ const queryImpl = debugCast ( query , QueryImpl ) ;
298+
299+ if ( ! queryImpl . memoizedAggregateTarget ) {
300+ // Do not include implicit order-bys for aggregate queries.
301+ queryImpl . memoizedAggregateTarget = _queryToTarget (
302+ queryImpl ,
303+ query . explicitOrderBy
304+ ) ;
305+ }
306+
307+ return queryImpl . memoizedAggregateTarget ;
308+ }
309+
310+ function _queryToTarget ( queryImpl : QueryImpl , orderBys : OrderBy [ ] ) : Target {
311+ if ( queryImpl . limitType === LimitType . First ) {
312+ return newTarget (
313+ queryImpl . path ,
314+ queryImpl . collectionGroup ,
315+ orderBys ,
316+ queryImpl . filters ,
317+ queryImpl . limit ,
318+ queryImpl . startAt ,
319+ queryImpl . endAt
320+ ) ;
321+ } else {
322+ // Flip the orderBy directions since we want the last results
323+ orderBys = orderBys . map ( orderBy => {
324+ const dir =
325+ orderBy . dir === Direction . DESCENDING
326+ ? Direction . ASCENDING
327+ : Direction . DESCENDING ;
328+ return new OrderBy ( orderBy . field , dir ) ;
329+ } ) ;
330+
331+ // We need to swap the cursors to match the now-flipped query ordering.
332+ const startAt = queryImpl . endAt
333+ ? new Bound ( queryImpl . endAt . position , queryImpl . endAt . inclusive )
334+ : null ;
335+ const endAt = queryImpl . startAt
336+ ? new Bound ( queryImpl . startAt . position , queryImpl . startAt . inclusive )
337+ : null ;
338+
339+ // Now return as a LimitType.First query.
340+ return newTarget (
341+ queryImpl . path ,
342+ queryImpl . collectionGroup ,
343+ orderBys ,
344+ queryImpl . filters ,
345+ queryImpl . limit ,
346+ startAt ,
347+ endAt
348+ ) ;
312349 }
313- return queryImpl . memoizedTarget ! ;
314350}
315351
316352export function queryWithAddedFilter ( query : Query , filter : Filter ) : Query {
@@ -461,14 +497,14 @@ function queryMatchesPathAndCollectionGroup(
461497 * in the results.
462498 */
463499function queryMatchesOrderBy ( query : Query , doc : Document ) : boolean {
464- // We must use `queryOrderBy ()` to get the list of all orderBys (both implicit and explicit).
500+ // We must use `queryNormalizedOrderBy ()` to get the list of all orderBys (both implicit and explicit).
465501 // Note that for OR queries, orderBy applies to all disjunction terms and implicit orderBys must
466502 // be taken into account. For example, the query "a > 1 || b==1" has an implicit "orderBy a" due
467503 // to the inequality, and is evaluated as "a > 1 orderBy a || b==1 orderBy a".
468504 // A document with content of {b:1} matches the filters, but does not match the orderBy because
469505 // it's missing the field 'a'.
470- for ( const orderBy of queryOrderBy ( query ) ) {
471- // order by key always matches
506+ for ( const orderBy of queryNormalizedOrderBy ( query ) ) {
507+ // order- by key always matches
472508 if ( ! orderBy . field . isKeyField ( ) && doc . data . field ( orderBy . field ) === null ) {
473509 return false ;
474510 }
@@ -489,13 +525,13 @@ function queryMatchesFilters(query: Query, doc: Document): boolean {
489525function queryMatchesBounds ( query : Query , doc : Document ) : boolean {
490526 if (
491527 query . startAt &&
492- ! boundSortsBeforeDocument ( query . startAt , queryOrderBy ( query ) , doc )
528+ ! boundSortsBeforeDocument ( query . startAt , queryNormalizedOrderBy ( query ) , doc )
493529 ) {
494530 return false ;
495531 }
496532 if (
497533 query . endAt &&
498- ! boundSortsAfterDocument ( query . endAt , queryOrderBy ( query ) , doc )
534+ ! boundSortsAfterDocument ( query . endAt , queryNormalizedOrderBy ( query ) , doc )
499535 ) {
500536 return false ;
501537 }
@@ -526,7 +562,7 @@ export function newQueryComparator(
526562) : ( d1 : Document , d2 : Document ) => number {
527563 return ( d1 : Document , d2 : Document ) : number => {
528564 let comparedOnKeyField = false ;
529- for ( const orderBy of queryOrderBy ( query ) ) {
565+ for ( const orderBy of queryNormalizedOrderBy ( query ) ) {
530566 const comp = compareDocs ( orderBy , d1 , d2 ) ;
531567 if ( comp !== 0 ) {
532568 return comp ;
0 commit comments