Skip to content

Commit ee871fc

Browse files
authored
Make count queries publicly available for use (#6608)
1 parent 516fe9c commit ee871fc

File tree

11 files changed

+443
-114
lines changed

11 files changed

+443
-114
lines changed

.changeset/wicked-tomatoes-grow.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@firebase/firestore': minor
3+
'firebase': minor
4+
---
5+
6+
Added `getCountFromServer()` (`getCount()` in the Lite SDK), which fetches the number of documents in the result set without actually downloading the documents.

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

+34
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,35 @@ export type AddPrefixToKeys<Prefix extends string, T extends Record<string, unkn
1717
[K in keyof T & string as `${Prefix}.${K}`]+?: T[K];
1818
};
1919

20+
// @public
21+
export class AggregateField<T> {
22+
type: string;
23+
}
24+
25+
// @public
26+
export type AggregateFieldType = AggregateField<number>;
27+
28+
// @public
29+
export class AggregateQuerySnapshot<T extends AggregateSpec> {
30+
data(): AggregateSpecData<T>;
31+
readonly query: Query<unknown>;
32+
readonly type = "AggregateQuerySnapshot";
33+
}
34+
35+
// @public
36+
export function aggregateQuerySnapshotEqual<T extends AggregateSpec>(left: AggregateQuerySnapshot<T>, right: AggregateQuerySnapshot<T>): boolean;
37+
38+
// @public
39+
export interface AggregateSpec {
40+
// (undocumented)
41+
[field: string]: AggregateFieldType;
42+
}
43+
44+
// @public
45+
export type AggregateSpecData<T extends AggregateSpec> = {
46+
[P in keyof T]: T[P] extends AggregateField<infer U> ? U : never;
47+
};
48+
2049
// @public
2150
export function arrayRemove(...elements: unknown[]): FieldValue;
2251

@@ -169,6 +198,11 @@ export class GeoPoint {
169198
};
170199
}
171200

201+
// @public
202+
export function getCount(query: Query<unknown>): Promise<AggregateQuerySnapshot<{
203+
count: AggregateField<number>;
204+
}>>;
205+
172206
// @public
173207
export function getDoc<T>(reference: DocumentReference<T>): Promise<DocumentSnapshot<T>>;
174208

common/api-review/firestore.api.md

+34
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,35 @@ export type AddPrefixToKeys<Prefix extends string, T extends Record<string, unkn
1717
[K in keyof T & string as `${Prefix}.${K}`]+?: T[K];
1818
};
1919

20+
// @public
21+
export class AggregateField<T> {
22+
type: string;
23+
}
24+
25+
// @public
26+
export type AggregateFieldType = AggregateField<number>;
27+
28+
// @public
29+
export class AggregateQuerySnapshot<T extends AggregateSpec> {
30+
data(): AggregateSpecData<T>;
31+
readonly query: Query<unknown>;
32+
readonly type = "AggregateQuerySnapshot";
33+
}
34+
35+
// @public
36+
export function aggregateQuerySnapshotEqual<T extends AggregateSpec>(left: AggregateQuerySnapshot<T>, right: AggregateQuerySnapshot<T>): boolean;
37+
38+
// @public
39+
export interface AggregateSpec {
40+
// (undocumented)
41+
[field: string]: AggregateFieldType;
42+
}
43+
44+
// @public
45+
export type AggregateSpecData<T extends AggregateSpec> = {
46+
[P in keyof T]: T[P] extends AggregateField<infer U> ? U : never;
47+
};
48+
2049
// @public
2150
export function arrayRemove(...elements: unknown[]): FieldValue;
2251

@@ -209,6 +238,11 @@ export class GeoPoint {
209238
};
210239
}
211240

241+
// @public
242+
export function getCountFromServer(query: Query<unknown>): Promise<AggregateQuerySnapshot<{
243+
count: AggregateField<number>;
244+
}>>;
245+
212246
// @public
213247
export function getDoc<T>(reference: DocumentReference<T>): Promise<DocumentSnapshot<T>>;
214248

packages/firestore/lite/index.ts

+13
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,19 @@
2727
import { registerFirestore } from './register';
2828
registerFirestore();
2929

30+
export {
31+
aggregateQuerySnapshotEqual,
32+
getCount
33+
} from '../src/lite-api/aggregate';
34+
35+
export {
36+
AggregateField,
37+
AggregateFieldType,
38+
AggregateSpec,
39+
AggregateSpecData,
40+
AggregateQuerySnapshot
41+
} from '../src/lite-api/aggregate_types';
42+
3043
export { FirestoreSettings as Settings } from '../src/lite-api/settings';
3144

3245
export {

packages/firestore/src/api.ts

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

18+
export {
19+
aggregateQuerySnapshotEqual,
20+
getCountFromServer
21+
} from './api/aggregate';
22+
23+
export {
24+
AggregateField,
25+
AggregateFieldType,
26+
AggregateSpec,
27+
AggregateSpecData,
28+
AggregateQuerySnapshot
29+
} from './lite-api/aggregate_types';
30+
1831
export { FieldPath, documentId } from './api/field_path';
1932

2033
export {

packages/firestore/src/api/aggregate.ts

+26-6
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,43 @@
1717

1818
import { Query } from '../api';
1919
import { firestoreClientRunCountQuery } from '../core/firestore_client';
20-
import { AggregateField, AggregateQuerySnapshot } from '../lite-api/aggregate';
20+
import {
21+
AggregateField,
22+
AggregateQuerySnapshot
23+
} from '../lite-api/aggregate_types';
2124
import { cast } from '../util/input_validation';
2225

2326
import { ensureFirestoreConfigured, Firestore } from './database';
27+
import { ExpUserDataWriter } from './reference_impl';
28+
29+
export { aggregateQuerySnapshotEqual } from '../lite-api/aggregate';
2430

2531
/**
26-
* Executes the query and returns the results as a `AggregateQuerySnapshot` from the
27-
* server. Returns an error if the network is not available.
32+
* Calculates the number of documents in the result set of the given query,
33+
* without actually downloading the documents.
34+
*
35+
* Using this function to count the documents is efficient because only the
36+
* final count, not the documents' data, is downloaded. This function can even
37+
* count the documents if the result set would be prohibitively large to
38+
* download entirely (e.g. thousands of documents).
2839
*
29-
* @param query - The `Query` to execute.
40+
* The result received from the server is presented, unaltered, without
41+
* considering any local state. That is, documents in the local cache are not
42+
* taken into consideration, neither are local modifications not yet
43+
* synchronized with the server. Previously-downloaded results, if any, are not
44+
* used: every request using this source necessarily involves a round trip to
45+
* the server.
3046
*
31-
* @returns A `Promise` that will be resolved with the results of the query.
47+
* @param query - The query whose result set size to calculate.
48+
* @returns A Promise that will be resolved with the count; the count can be
49+
* retrieved from `snapshot.data().count`, where `snapshot` is the
50+
* `AggregateQuerySnapshot` to which the returned Promise resolves.
3251
*/
3352
export function getCountFromServer(
3453
query: Query<unknown>
3554
): Promise<AggregateQuerySnapshot<{ count: AggregateField<number> }>> {
3655
const firestore = cast(query.firestore, Firestore);
3756
const client = ensureFirestoreConfigured(firestore);
38-
return firestoreClientRunCountQuery(client, query);
57+
const userDataWriter = new ExpUserDataWriter(firestore);
58+
return firestoreClientRunCountQuery(client, query, userDataWriter);
3959
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* @license
3+
* Copyright 2022 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 { AbstractUserDataWriter, Query } from '../api';
19+
import {
20+
AggregateField,
21+
AggregateQuerySnapshot
22+
} from '../lite-api/aggregate_types';
23+
import { Value } from '../protos/firestore_proto_api';
24+
import { Datastore, invokeRunAggregationQueryRpc } from '../remote/datastore';
25+
import { hardAssert } from '../util/assert';
26+
27+
/**
28+
* CountQueryRunner encapsulates the logic needed to run the count aggregation
29+
* queries.
30+
*/
31+
export class CountQueryRunner {
32+
constructor(
33+
private readonly query: Query<unknown>,
34+
private readonly datastore: Datastore,
35+
private readonly userDataWriter: AbstractUserDataWriter
36+
) {}
37+
38+
run(): Promise<AggregateQuerySnapshot<{ count: AggregateField<number> }>> {
39+
return invokeRunAggregationQueryRpc(this.datastore, this.query._query).then(
40+
result => {
41+
hardAssert(
42+
result[0] !== undefined,
43+
'Aggregation fields are missing from result.'
44+
);
45+
46+
const counts = Object.entries(result[0])
47+
.filter(([key, value]) => key === 'count_alias')
48+
.map(([key, value]) =>
49+
this.userDataWriter.convertValue(value as Value)
50+
);
51+
52+
const countValue = counts[0];
53+
54+
hardAssert(
55+
typeof countValue === 'number',
56+
'Count aggregate field value is not a number: ' + countValue
57+
);
58+
59+
return Promise.resolve(
60+
new AggregateQuerySnapshot<{ count: AggregateField<number> }>(
61+
this.query,
62+
{
63+
count: countValue
64+
}
65+
)
66+
);
67+
}
68+
);
69+
}
70+
}

packages/firestore/src/core/firestore_client.ts

+14-7
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,17 @@
1717

1818
import { GetOptions } from '@firebase/firestore-types';
1919

20+
import {
21+
AbstractUserDataWriter,
22+
AggregateField,
23+
AggregateQuerySnapshot
24+
} from '../api';
2025
import { LoadBundleTask } from '../api/bundle';
2126
import {
2227
CredentialChangeListener,
2328
CredentialsProvider
2429
} from '../api/credentials';
2530
import { User } from '../auth/user';
26-
import {
27-
AggregateField,
28-
AggregateQuerySnapshot,
29-
getCount
30-
} from '../lite-api/aggregate';
3131
import { Query as LiteQuery } from '../lite-api/reference';
3232
import { LocalStore } from '../local/local_store';
3333
import {
@@ -68,6 +68,7 @@ import {
6868
OfflineComponentProvider,
6969
OnlineComponentProvider
7070
} from './component_provider';
71+
import { CountQueryRunner } from './count_query_runner';
7172
import { DatabaseId, DatabaseInfo } from './database_info';
7273
import {
7374
addSnapshotsInSyncListener,
@@ -510,7 +511,8 @@ export function firestoreClientTransaction<T>(
510511

511512
export function firestoreClientRunCountQuery(
512513
client: FirestoreClient,
513-
query: LiteQuery<unknown>
514+
query: LiteQuery<unknown>,
515+
userDataWriter: AbstractUserDataWriter
514516
): Promise<AggregateQuerySnapshot<{ count: AggregateField<number> }>> {
515517
const deferred = new Deferred<
516518
AggregateQuerySnapshot<{ count: AggregateField<number> }>
@@ -526,7 +528,12 @@ export function firestoreClientRunCountQuery(
526528
)
527529
);
528530
} else {
529-
const result = await getCount(query);
531+
const datastore = await getDatastore(client);
532+
const result = new CountQueryRunner(
533+
query,
534+
datastore,
535+
userDataWriter
536+
).run();
530537
deferred.resolve(result);
531538
}
532539
} catch (e) {

0 commit comments

Comments
 (0)