diff --git a/.changeset/beige-oranges-eat.md b/.changeset/beige-oranges-eat.md new file mode 100644 index 00000000000..b4a9dc54390 --- /dev/null +++ b/.changeset/beige-oranges-eat.md @@ -0,0 +1,6 @@ +--- +"@firebase/firestore": minor +"firebase": minor +--- + +Support sum and average aggregations. diff --git a/common/api-review/firestore-lite.api.md b/common/api-review/firestore-lite.api.md index 337c413f5a2..440fa488c1c 100644 --- a/common/api-review/firestore-lite.api.md +++ b/common/api-review/firestore-lite.api.md @@ -19,11 +19,15 @@ export type AddPrefixToKeys { + readonly aggregateType: AggregateType; readonly type = "AggregateField"; } // @public -export type AggregateFieldType = AggregateField; +export function aggregateFieldEqual(left: AggregateField, right: AggregateField): boolean; + +// @public +export type AggregateFieldType = ReturnType | ReturnType | ReturnType; // @public export class AggregateQuerySnapshot { @@ -46,6 +50,9 @@ export type AggregateSpecData = { [P in keyof T]: T[P] extends AggregateField ? U : never; }; +// @public +export type AggregateType = 'count' | 'avg' | 'sum'; + // @public export function and(...queryConstraints: QueryFilterConstraint[]): QueryCompositeFilterConstraint; @@ -55,6 +62,9 @@ export function arrayRemove(...elements: unknown[]): FieldValue; // @public export function arrayUnion(...elements: unknown[]): FieldValue; +// @public +export function average(field: string | FieldPath): AggregateField; + // @public export class Bytes { static fromBase64String(base64: string): Bytes; @@ -95,6 +105,9 @@ export function connectFirestoreEmulator(firestore: Firestore, host: string, por mockUserToken?: EmulatorMockTokenOptions | string; }): void; +// @public +export function count(): AggregateField; + // @public export function deleteDoc(reference: DocumentReference): Promise; @@ -201,6 +214,9 @@ export class GeoPoint { }; } +// @public +export function getAggregate(query: Query, aggregateSpec: AggregateSpecType): Promise>; + // @public export function getCount(query: Query): Promise; @@ -388,6 +404,9 @@ export function startAt(snapshot // @public export function startAt(...fieldValues: unknown[]): QueryStartAtConstraint; +// @public +export function sum(field: string | FieldPath): AggregateField; + // @public export function terminate(firestore: Firestore): Promise; diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index 572c8f712a2..7d62658ecc1 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -19,11 +19,15 @@ export type AddPrefixToKeys { + readonly aggregateType: AggregateType; readonly type = "AggregateField"; } // @public -export type AggregateFieldType = AggregateField; +export function aggregateFieldEqual(left: AggregateField, right: AggregateField): boolean; + +// @public +export type AggregateFieldType = ReturnType | ReturnType | ReturnType; // @public export class AggregateQuerySnapshot { @@ -46,6 +50,9 @@ export type AggregateSpecData = { [P in keyof T]: T[P] extends AggregateField ? U : never; }; +// @public +export type AggregateType = 'count' | 'avg' | 'sum'; + // @public export function and(...queryConstraints: QueryFilterConstraint[]): QueryCompositeFilterConstraint; @@ -55,6 +62,9 @@ export function arrayRemove(...elements: unknown[]): FieldValue; // @public export function arrayUnion(...elements: unknown[]): FieldValue; +// @public +export function average(field: string | FieldPath): AggregateField; + // @public export class Bytes { static fromBase64String(base64: string): Bytes; @@ -101,6 +111,9 @@ export function connectFirestoreEmulator(firestore: Firestore, host: string, por mockUserToken?: EmulatorMockTokenOptions | string; }): void; +// @public +export function count(): AggregateField; + // @public export function deleteAllPersistentCacheIndexes(indexManager: PersistentCacheIndexManager): void; @@ -260,6 +273,9 @@ export class GeoPoint { }; } +// @public +export function getAggregateFromServer(query: Query, aggregateSpec: AggregateSpecType): Promise>; + // @public export function getCountFromServer(query: Query): Promise; @@ -661,6 +677,9 @@ export function startAt(snapshot // @public export function startAt(...fieldValues: unknown[]): QueryStartAtConstraint; +// @public +export function sum(field: string | FieldPath): AggregateField; + // @public export type TaskState = 'Error' | 'Running' | 'Success'; diff --git a/docs-devsite/firestore_.aggregatefield.md b/docs-devsite/firestore_.aggregatefield.md index 9d364a6abcc..e8a4e47ad77 100644 --- a/docs-devsite/firestore_.aggregatefield.md +++ b/docs-devsite/firestore_.aggregatefield.md @@ -22,8 +22,19 @@ export declare class AggregateField | Property | Modifiers | Type | Description | | --- | --- | --- | --- | +| [aggregateType](./firestore_.aggregatefield.md#aggregatefieldaggregatetype) | | [AggregateType](./firestore_.md#aggregatetype) | Indicates the aggregation operation of this AggregateField. | | [type](./firestore_.aggregatefield.md#aggregatefieldtype) | | (not declared) | A type string to uniquely identify instances of this class. | +## AggregateField.aggregateType + +Indicates the aggregation operation of this AggregateField. + +Signature: + +```typescript +readonly aggregateType: AggregateType; +``` + ## AggregateField.type A type string to uniquely identify instances of this class. diff --git a/docs-devsite/firestore_.md b/docs-devsite/firestore_.md index 6fdd2abdd05..a90b41b3483 100644 --- a/docs-devsite/firestore_.md +++ b/docs-devsite/firestore_.md @@ -41,6 +41,7 @@ https://github.com/firebase/firebase-js-sdk | [waitForPendingWrites(firestore)](./firestore_.md#waitforpendingwrites) | Waits until all currently pending writes for the active user have been acknowledged by the backend.The returned promise resolves immediately if there are no outstanding writes. Otherwise, the promise waits for all previously issued writes (including those written in a previous app session), but it does not wait for writes that were added after the function is called. If you want to wait for additional writes, call waitForPendingWrites() again.Any outstanding waitForPendingWrites() promises are rejected during user changes. | | [writeBatch(firestore)](./firestore_.md#writebatch) | Creates a write batch, used for performing multiple writes as a single atomic operation. The maximum number of writes allowed in a single [WriteBatch](./firestore_.writebatch.md#writebatch_class) is 500.Unlike transactions, write batches are persisted offline and therefore are preferable when you don't need to condition your writes on read data. | | function() | +| [count()](./firestore_.md#count) | Create an AggregateField object that can be used to compute the count of documents in the result set of a query. | | [deleteField()](./firestore_.md#deletefield) | Returns a sentinel for use with [updateDoc()](./firestore_lite.md#updatedoc) or [setDoc()](./firestore_lite.md#setdoc) with {merge: true} to mark a field for deletion. | | [documentId()](./firestore_.md#documentid) | Returns a special sentinel FieldPath to refer to the ID of a document. It can be used in queries to sort or filter by the document ID. | | [getFirestore()](./firestore_.md#getfirestore) | Returns the existing default [Firestore](./firestore_.firestore.md#firestore_class) instance that is associated with the default [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface). If no instance exists, initializes a new instance with default settings. | @@ -52,6 +53,9 @@ https://github.com/firebase/firebase-js-sdk | function(elements...) | | [arrayRemove(elements)](./firestore_.md#arrayremove) | Returns a special value that can be used with [setDoc()](./firestore_.md#setdoc) or that tells the server to remove the given elements from any array value that already exists on the server. All instances of each element specified will be removed from the array. If the field being modified is not already an array it will be overwritten with an empty array. | | [arrayUnion(elements)](./firestore_.md#arrayunion) | Returns a special value that can be used with [setDoc()](./firestore_lite.md#setdoc) or [updateDoc()](./firestore_lite.md#updatedoc) that tells the server to union the given elements with any array value that already exists on the server. Each specified element that doesn't already exist in the array will be added to the end. If the field being modified is not already an array it will be overwritten with an array containing exactly the specified elements. | +| function(field...) | +| [average(field)](./firestore_.md#average) | Create an AggregateField object that can be used to compute the average of a specified field over a range of documents in the result set of a query. | +| [sum(field)](./firestore_.md#sum) | Create an AggregateField object that can be used to compute the sum of a specified field over a range of documents in the result set of a query. | | function(fieldPath...) | | [orderBy(fieldPath, directionStr)](./firestore_.md#orderby) | Creates a [QueryOrderByConstraint](./firestore_.queryorderbyconstraint.md#queryorderbyconstraint_class) that sorts the query result by the specified field, optionally in descending order instead of ascending.Note: Documents that do not contain the specified field will not be present in the query result. | | [where(fieldPath, opStr, value)](./firestore_.md#where) | Creates a [QueryFieldFilterConstraint](./firestore_.queryfieldfilterconstraint.md#queryfieldfilterconstraint_class) that enforces that documents must contain the specified field and that the value should satisfy the relation constraint provided. | @@ -65,6 +69,7 @@ https://github.com/firebase/firebase-js-sdk | [disablePersistentCacheIndexAutoCreation(indexManager)](./firestore_.md#disablepersistentcacheindexautocreation) | Stops creating persistent cache indexes automatically for local query execution. The indexes which have been created by calling enablePersistentCacheIndexAutoCreation() still take effect. | | [enablePersistentCacheIndexAutoCreation(indexManager)](./firestore_.md#enablepersistentcacheindexautocreation) | Enables the SDK to create persistent cache indexes automatically for local query execution when the SDK believes cache indexes can help improve performance.This feature is disabled by default. | | function(left...) | +| [aggregateFieldEqual(left, right)](./firestore_.md#aggregatefieldequal) | Compares two 'AggregateField\` instances for equality. | | [aggregateQuerySnapshotEqual(left, right)](./firestore_.md#aggregatequerysnapshotequal) | Compares two AggregateQuerySnapshot instances for equality.Two AggregateQuerySnapshot instances are considered "equal" if they have underlying queries that compare equal, and the same data. | | [queryEqual(left, right)](./firestore_.md#queryequal) | Returns true if the provided queries point to the same collection and apply the same constraints. | | [refEqual(left, right)](./firestore_.md#refequal) | Returns true if the provided references are equal. | @@ -77,6 +82,7 @@ https://github.com/firebase/firebase-js-sdk | function(n...) | | [increment(n)](./firestore_.md#increment) | Returns a special value that can be used with [setDoc()](./firestore_lite.md#setdoc) or [updateDoc()](./firestore_lite.md#updatedoc) that tells the server to increment the field's current value by the given value.If either the operand or the current field value uses floating point precision, all arithmetic follows IEEE 754 semantics. If both values are integers, values outside of JavaScript's safe number range (Number.MIN_SAFE_INTEGER to Number.MAX_SAFE_INTEGER) are also subject to precision loss. Furthermore, once processed by the Firestore backend, all integer operations are capped between -2^63 and 2^63-1.If the current field value is not of type number, or if the field does not yet exist, the transformation sets the field to the given value. | | function(query...) | +| [getAggregateFromServer(query, aggregateSpec)](./firestore_.md#getaggregatefromserver) | Calculates the specified aggregations over the documents in the result set of the given query, without actually downloading the documents.Using this function to perform aggregations is efficient because only the final aggregation values, not the documents' data, are downloaded. This function can even perform aggregations of the documents if the result set would be prohibitively large to download entirely (e.g. thousands of documents).The result received from the server is presented, unaltered, without considering any local state. That is, documents in the local cache are not taken into consideration, neither are local modifications not yet synchronized with the server. Previously-downloaded results, if any, are not used: every request using this source necessarily involves a round trip to the server. | | [getCountFromServer(query)](./firestore_.md#getcountfromserver) | Calculates the number of documents in the result set of the given query, without actually downloading the documents.Using this function to count the documents is efficient because only the final count, not the documents' data, is downloaded. This function can even count the documents if the result set would be prohibitively large to download entirely (e.g. thousands of documents).The result received from the server is presented, unaltered, without considering any local state. That is, documents in the local cache are not taken into consideration, neither are local modifications not yet synchronized with the server. Previously-downloaded results, if any, are not used: every request using this source necessarily involves a round trip to the server. | | [getDocs(query)](./firestore_.md#getdocs) | Executes the query and returns the results as a QuerySnapshot.Note: getDocs() attempts to provide up-to-date data when possible by waiting for data from the server, but it may return cached data or fail if you are offline and the server cannot be reached. To specify this behavior, invoke [getDocsFromCache()](./firestore_.md#getdocsfromcache) or [getDocsFromServer()](./firestore_.md#getdocsfromserver). | | [getDocsFromCache(query)](./firestore_.md#getdocsfromcache) | Executes the query and returns the results as a QuerySnapshot from cache. Returns an empty result set if no documents matching the query are currently cached. | @@ -193,6 +199,7 @@ https://github.com/firebase/firebase-js-sdk | [AddPrefixToKeys](./firestore_.md#addprefixtokeys) | Returns a new map where every key is prefixed with the outer key appended to a dot. | | [AggregateFieldType](./firestore_.md#aggregatefieldtype) | The union of all AggregateField types that are supported by Firestore. | | [AggregateSpecData](./firestore_.md#aggregatespecdata) | A type whose keys are taken from an AggregateSpec, and whose values are the result of the aggregation performed by the corresponding AggregateField from the input AggregateSpec. | +| [AggregateType](./firestore_.md#aggregatetype) | Union type representing the aggregate type to be performed. | | [ChildUpdateFields](./firestore_.md#childupdatefields) | Helper for calculating the nested fields for a given type T1. This is needed to distribute union types such as undefined | {...} (happens for optional props) or {a: A} | {b: B}.In this use case, V is used to distribute the union types of T[K] on Record, since T[K] is evaluated as an expression and not distributed.See https://www.typescriptlang.org/docs/handbook/advanced-types.html\#distributive-conditional-types | | [DocumentChangeType](./firestore_.md#documentchangetype) | The type of a DocumentChange may be 'added', 'removed', or 'modified'. | | [FirestoreErrorCode](./firestore_.md#firestoreerrorcode) | The set of Firestore status codes. The codes are the same at the ones exposed by gRPC here: https://github.com/grpc/grpc/blob/master/doc/statuscodes.mdPossible values: - 'cancelled': The operation was cancelled (typically by the caller). - 'unknown': Unknown error or an error from a different error domain. - 'invalid-argument': Client specified an invalid argument. Note that this differs from 'failed-precondition'. 'invalid-argument' indicates arguments that are problematic regardless of the state of the system (e.g. an invalid field name). - 'deadline-exceeded': Deadline expired before operation could complete. For operations that change the state of the system, this error may be returned even if the operation has completed successfully. For example, a successful response from a server could have been delayed long enough for the deadline to expire. - 'not-found': Some requested document was not found. - 'already-exists': Some document that we attempted to create already exists. - 'permission-denied': The caller does not have permission to execute the specified operation. - 'resource-exhausted': Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system is out of space. - 'failed-precondition': Operation was rejected because the system is not in a state required for the operation's execution. - 'aborted': The operation was aborted, typically due to a concurrency issue like transaction aborts, etc. - 'out-of-range': Operation was attempted past the valid range. - 'unimplemented': Operation is not implemented or not supported/enabled. - 'internal': Internal errors. Means some invariants expected by underlying system has been broken. If you see one of these errors, something is very broken. - 'unavailable': The service is currently unavailable. This is most likely a transient condition and may be corrected by retrying with a backoff. - 'data-loss': Unrecoverable data loss or corruption. - 'unauthenticated': The request does not have valid authentication credentials for the operation. | @@ -842,6 +849,19 @@ export declare function writeBatch(firestore: Firestore): WriteBatch; A [WriteBatch](./firestore_.writebatch.md#writebatch_class) that can be used to atomically execute multiple writes. +## count() + +Create an AggregateField object that can be used to compute the count of documents in the result set of a query. + +Signature: + +```typescript +export declare function count(): AggregateField; +``` +Returns: + +[AggregateField](./firestore_.aggregatefield.md#aggregatefield_class)<number> + ## deleteField() Returns a sentinel for use with [updateDoc()](./firestore_lite.md#updatedoc) or [setDoc()](./firestore_lite.md#setdoc) with `{merge: true}` to mark a field for deletion. @@ -991,6 +1011,46 @@ export declare function arrayUnion(...elements: unknown[]): FieldValue; The `FieldValue` sentinel for use in a call to `setDoc()` or `updateDoc()`. +## average() + +Create an AggregateField object that can be used to compute the average of a specified field over a range of documents in the result set of a query. + +Signature: + +```typescript +export declare function average(field: string | FieldPath): AggregateField; +``` + +### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| field | string \| [FieldPath](./firestore_.fieldpath.md#fieldpath_class) | Specifies the field to average across the result set. | + +Returns: + +[AggregateField](./firestore_.aggregatefield.md#aggregatefield_class)<number \| null> + +## sum() + +Create an AggregateField object that can be used to compute the sum of a specified field over a range of documents in the result set of a query. + +Signature: + +```typescript +export declare function sum(field: string | FieldPath): AggregateField; +``` + +### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| field | string \| [FieldPath](./firestore_.fieldpath.md#fieldpath_class) | Specifies the field to sum across the result set. | + +Returns: + +[AggregateField](./firestore_.aggregatefield.md#aggregatefield_class)<number> + ## orderBy() Creates a [QueryOrderByConstraint](./firestore_.queryorderbyconstraint.md#queryorderbyconstraint_class) that sorts the query result by the specified field, optionally in descending order instead of ascending. @@ -1192,6 +1252,27 @@ export declare function enablePersistentCacheIndexAutoCreation(indexManager: Per void +## aggregateFieldEqual() + +Compares two 'AggregateField\` instances for equality. + +Signature: + +```typescript +export declare function aggregateFieldEqual(left: AggregateField, right: AggregateField): boolean; +``` + +### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| left | [AggregateField](./firestore_.aggregatefield.md#aggregatefield_class)<unknown> | Compare this AggregateField to the right. | +| right | [AggregateField](./firestore_.aggregatefield.md#aggregatefield_class)<unknown> | Compare this AggregateField to the left. | + +Returns: + +boolean + ## aggregateQuerySnapshotEqual() Compares two `AggregateQuerySnapshot` instances for equality. @@ -1378,6 +1459,47 @@ export declare function increment(n: number): FieldValue; The `FieldValue` sentinel for use in a call to `setDoc()` or `updateDoc()` +## getAggregateFromServer() + +Calculates the specified aggregations over the documents in the result set of the given query, without actually downloading the documents. + +Using this function to perform aggregations is efficient because only the final aggregation values, not the documents' data, are downloaded. This function can even perform aggregations of the documents if the result set would be prohibitively large to download entirely (e.g. thousands of documents). + +The result received from the server is presented, unaltered, without considering any local state. That is, documents in the local cache are not taken into consideration, neither are local modifications not yet synchronized with the server. Previously-downloaded results, if any, are not used: every request using this source necessarily involves a round trip to the server. + +Signature: + +```typescript +export declare function getAggregateFromServer(query: Query, aggregateSpec: AggregateSpecType): Promise>; +``` + +### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| query | [Query](./firestore_.query.md#query_class)<AppModelType, DbModelType> | The query whose result set to aggregate over. | +| aggregateSpec | AggregateSpecType | An AggregateSpec object that specifies the aggregates to perform over the result set. The AggregateSpec specifies aliases for each aggregate, which can be used to retrieve the aggregate result. | + +Returns: + +Promise<[AggregateQuerySnapshot](./firestore_.aggregatequerysnapshot.md#aggregatequerysnapshot_class)<AggregateSpecType, AppModelType, DbModelType>> + +### Example + + +```typescript +const aggregateSnapshot = await getAggregateFromServer(query, { + countOfDocs: count(), + totalHours: sum('hours'), + averageScore: average('score') +}); + +const countOfDocs: number = aggregateSnapshot.data().countOfDocs; +const totalHours: number = aggregateSnapshot.data().totalHours; +const averageScore: number | null = aggregateSnapshot.data().averageScore; + +``` + ## getCountFromServer() Calculates the number of documents in the result set of the given query, without actually downloading the documents. @@ -2322,7 +2444,7 @@ The union of all `AggregateField` types that are supported by Firestore. Signature: ```typescript -export declare type AggregateFieldType = AggregateField; +export declare type AggregateFieldType = ReturnType | ReturnType | ReturnType; ``` ## AggregateSpecData @@ -2337,6 +2459,16 @@ export declare type AggregateSpecData = { }; ``` +## AggregateType + +Union type representing the aggregate type to be performed. + +Signature: + +```typescript +export declare type AggregateType = 'count' | 'avg' | 'sum'; +``` + ## ChildUpdateFields Helper for calculating the nested fields for a given type T1. This is needed to distribute union types such as `undefined | {...}` (happens for optional props) or `{a: A} | {b: B}`. diff --git a/docs-devsite/firestore_lite.aggregatefield.md b/docs-devsite/firestore_lite.aggregatefield.md index 2376c50c41f..1bf831fca29 100644 --- a/docs-devsite/firestore_lite.aggregatefield.md +++ b/docs-devsite/firestore_lite.aggregatefield.md @@ -22,8 +22,19 @@ export declare class AggregateField | Property | Modifiers | Type | Description | | --- | --- | --- | --- | +| [aggregateType](./firestore_lite.aggregatefield.md#aggregatefieldaggregatetype) | | [AggregateType](./firestore_lite.md#aggregatetype) | Indicates the aggregation operation of this AggregateField. | | [type](./firestore_lite.aggregatefield.md#aggregatefieldtype) | | (not declared) | A type string to uniquely identify instances of this class. | +## AggregateField.aggregateType + +Indicates the aggregation operation of this AggregateField. + +Signature: + +```typescript +readonly aggregateType: AggregateType; +``` + ## AggregateField.type A type string to uniquely identify instances of this class. diff --git a/docs-devsite/firestore_lite.md b/docs-devsite/firestore_lite.md index e6ba851bbd7..e480a51ddf6 100644 --- a/docs-devsite/firestore_lite.md +++ b/docs-devsite/firestore_lite.md @@ -29,6 +29,7 @@ https://github.com/firebase/firebase-js-sdk | [terminate(firestore)](./firestore_lite.md#terminate) | Terminates the provided Firestore instance.After calling terminate() only the clearIndexedDbPersistence() functions may be used. Any other function will throw a FirestoreError. Termination does not cancel any pending writes, and any promises that are awaiting a response from the server will not be resolved.To restart after termination, create a new instance of Firestore with [getFirestore()](./firestore_.md#getfirestore).Note: Under normal circumstances, calling terminate() is not required. This function is useful only when you want to force this instance to release all of its resources or in combination with [clearIndexedDbPersistence()](./firestore_.md#clearindexeddbpersistence) to ensure that all local state is destroyed between test runs. | | [writeBatch(firestore)](./firestore_lite.md#writebatch) | Creates a write batch, used for performing multiple writes as a single atomic operation. The maximum number of writes allowed in a single WriteBatch is 500.The result of these writes will only be reflected in document reads that occur after the returned promise resolves. If the client is offline, the write fails. If you would like to see local modifications or buffer writes until the client is online, use the full Firestore SDK. | | function() | +| [count()](./firestore_lite.md#count) | Create an AggregateField object that can be used to compute the count of documents in the result set of a query. | | [deleteField()](./firestore_lite.md#deletefield) | Returns a sentinel for use with [updateDoc()](./firestore_lite.md#updatedoc) or [setDoc()](./firestore_lite.md#setdoc) with {merge: true} to mark a field for deletion. | | [documentId()](./firestore_lite.md#documentid) | Returns a special sentinel FieldPath to refer to the ID of a document. It can be used in queries to sort or filter by the document ID. | | [getFirestore()](./firestore_lite.md#getfirestore) | Returns the existing default [Firestore](./firestore_.firestore.md#firestore_class) instance that is associated with the default [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface). If no instance exists, initializes a new instance with default settings. | @@ -38,6 +39,9 @@ https://github.com/firebase/firebase-js-sdk | function(elements...) | | [arrayRemove(elements)](./firestore_lite.md#arrayremove) | Returns a special value that can be used with [setDoc()](./firestore_.md#setdoc) or that tells the server to remove the given elements from any array value that already exists on the server. All instances of each element specified will be removed from the array. If the field being modified is not already an array it will be overwritten with an empty array. | | [arrayUnion(elements)](./firestore_lite.md#arrayunion) | Returns a special value that can be used with [setDoc()](./firestore_lite.md#setdoc) or [updateDoc()](./firestore_lite.md#updatedoc) that tells the server to union the given elements with any array value that already exists on the server. Each specified element that doesn't already exist in the array will be added to the end. If the field being modified is not already an array it will be overwritten with an array containing exactly the specified elements. | +| function(field...) | +| [average(field)](./firestore_lite.md#average) | Create an AggregateField object that can be used to compute the average of a specified field over a range of documents in the result set of a query. | +| [sum(field)](./firestore_lite.md#sum) | Create an AggregateField object that can be used to compute the sum of a specified field over a range of documents in the result set of a query. | | function(fieldPath...) | | [orderBy(fieldPath, directionStr)](./firestore_lite.md#orderby) | Creates a [QueryOrderByConstraint](./firestore_.queryorderbyconstraint.md#queryorderbyconstraint_class) that sorts the query result by the specified field, optionally in descending order instead of ascending.Note: Documents that do not contain the specified field will not be present in the query result. | | [where(fieldPath, opStr, value)](./firestore_lite.md#where) | Creates a [QueryFieldFilterConstraint](./firestore_.queryfieldfilterconstraint.md#queryfieldfilterconstraint_class) that enforces that documents must contain the specified field and that the value should satisfy the relation constraint provided. | @@ -47,6 +51,7 @@ https://github.com/firebase/firebase-js-sdk | [startAfter(fieldValues)](./firestore_lite.md#startafter) | Creates a [QueryStartAtConstraint](./firestore_.querystartatconstraint.md#querystartatconstraint_class) that modifies the result set to start after the provided fields relative to the order of the query. The order of the field values must match the order of the order by clauses of the query. | | [startAt(fieldValues)](./firestore_lite.md#startat) | Creates a [QueryStartAtConstraint](./firestore_.querystartatconstraint.md#querystartatconstraint_class) that modifies the result set to start at the provided fields relative to the order of the query. The order of the field values must match the order of the order by clauses of the query. | | function(left...) | +| [aggregateFieldEqual(left, right)](./firestore_lite.md#aggregatefieldequal) | Compares two 'AggregateField\` instances for equality. | | [aggregateQuerySnapshotEqual(left, right)](./firestore_lite.md#aggregatequerysnapshotequal) | Compares two AggregateQuerySnapshot instances for equality.Two AggregateQuerySnapshot instances are considered "equal" if they have underlying queries that compare equal, and the same data. | | [queryEqual(left, right)](./firestore_lite.md#queryequal) | Returns true if the provided queries point to the same collection and apply the same constraints. | | [refEqual(left, right)](./firestore_lite.md#refequal) | Returns true if the provided references are equal. | @@ -59,6 +64,7 @@ https://github.com/firebase/firebase-js-sdk | function(n...) | | [increment(n)](./firestore_lite.md#increment) | Returns a special value that can be used with [setDoc()](./firestore_lite.md#setdoc) or [updateDoc()](./firestore_lite.md#updatedoc) that tells the server to increment the field's current value by the given value.If either the operand or the current field value uses floating point precision, all arithmetic follows IEEE 754 semantics. If both values are integers, values outside of JavaScript's safe number range (Number.MIN_SAFE_INTEGER to Number.MAX_SAFE_INTEGER) are also subject to precision loss. Furthermore, once processed by the Firestore backend, all integer operations are capped between -2^63 and 2^63-1.If the current field value is not of type number, or if the field does not yet exist, the transformation sets the field to the given value. | | function(query...) | +| [getAggregate(query, aggregateSpec)](./firestore_lite.md#getaggregate) | Calculates the specified aggregations over the documents in the result set of the given query, without actually downloading the documents.Using this function to perform aggregations is efficient because only the final aggregation values, not the documents' data, are downloaded. This function can even perform aggregations of the documents if the result set would be prohibitively large to download entirely (e.g. thousands of documents). | | [getCount(query)](./firestore_lite.md#getcount) | Calculates the number of documents in the result set of the given query, without actually downloading the documents.Using this function to count the documents is efficient because only the final count, not the documents' data, is downloaded. This function can even count the documents if the result set would be prohibitively large to download entirely (e.g. thousands of documents). | | [getDocs(query)](./firestore_lite.md#getdocs) | Executes the query and returns the results as a [QuerySnapshot](./firestore_.querysnapshot.md#querysnapshot_class).All queries are executed directly by the server, even if the the query was previously executed. Recent modifications are only reflected in the retrieved results if they have already been applied by the backend. If the client is offline, the operation fails. To see previously cached result and local modifications, use the full Firestore SDK. | | [query(query, compositeFilter, queryConstraints)](./firestore_lite.md#query) | Creates a new immutable instance of [Query](./firestore_.query.md#query_class) that is extended to also include additional query constraints. | @@ -130,6 +136,7 @@ https://github.com/firebase/firebase-js-sdk | [AddPrefixToKeys](./firestore_lite.md#addprefixtokeys) | Returns a new map where every key is prefixed with the outer key appended to a dot. | | [AggregateFieldType](./firestore_lite.md#aggregatefieldtype) | The union of all AggregateField types that are supported by Firestore. | | [AggregateSpecData](./firestore_lite.md#aggregatespecdata) | A type whose keys are taken from an AggregateSpec, and whose values are the result of the aggregation performed by the corresponding AggregateField from the input AggregateSpec. | +| [AggregateType](./firestore_lite.md#aggregatetype) | Union type representing the aggregate type to be performed. | | [ChildUpdateFields](./firestore_lite.md#childupdatefields) | Helper for calculating the nested fields for a given type T1. This is needed to distribute union types such as undefined | {...} (happens for optional props) or {a: A} | {b: B}.In this use case, V is used to distribute the union types of T[K] on Record, since T[K] is evaluated as an expression and not distributed.See https://www.typescriptlang.org/docs/handbook/advanced-types.html\#distributive-conditional-types | | [FirestoreErrorCode](./firestore_lite.md#firestoreerrorcode) | The set of Firestore status codes. The codes are the same at the ones exposed by gRPC here: https://github.com/grpc/grpc/blob/master/doc/statuscodes.mdPossible values: - 'cancelled': The operation was cancelled (typically by the caller). - 'unknown': Unknown error or an error from a different error domain. - 'invalid-argument': Client specified an invalid argument. Note that this differs from 'failed-precondition'. 'invalid-argument' indicates arguments that are problematic regardless of the state of the system (e.g. an invalid field name). - 'deadline-exceeded': Deadline expired before operation could complete. For operations that change the state of the system, this error may be returned even if the operation has completed successfully. For example, a successful response from a server could have been delayed long enough for the deadline to expire. - 'not-found': Some requested document was not found. - 'already-exists': Some document that we attempted to create already exists. - 'permission-denied': The caller does not have permission to execute the specified operation. - 'resource-exhausted': Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system is out of space. - 'failed-precondition': Operation was rejected because the system is not in a state required for the operation's execution. - 'aborted': The operation was aborted, typically due to a concurrency issue like transaction aborts, etc. - 'out-of-range': Operation was attempted past the valid range. - 'unimplemented': Operation is not implemented or not supported/enabled. - 'internal': Internal errors. Means some invariants expected by underlying system has been broken. If you see one of these errors, something is very broken. - 'unavailable': The service is currently unavailable. This is most likely a transient condition and may be corrected by retrying with a backoff. - 'data-loss': Unrecoverable data loss or corruption. - 'unauthenticated': The request does not have valid authentication credentials for the operation. | | [NestedUpdateFields](./firestore_lite.md#nestedupdatefields) | For each field (e.g. 'bar'), find all nested keys (e.g. {'bar.baz': T1, 'bar.qux': T2}). Intersect them together to make a single map containing all possible keys that are all marked as optional | @@ -427,6 +434,19 @@ export declare function writeBatch(firestore: Firestore): WriteBatch; A `WriteBatch` that can be used to atomically execute multiple writes. +## count() + +Create an AggregateField object that can be used to compute the count of documents in the result set of a query. + +Signature: + +```typescript +export declare function count(): AggregateField; +``` +Returns: + +[AggregateField](./firestore_lite.aggregatefield.md#aggregatefield_class)<number> + ## deleteField() Returns a sentinel for use with [updateDoc()](./firestore_lite.md#updatedoc) or [setDoc()](./firestore_lite.md#setdoc) with `{merge: true}` to mark a field for deletion. @@ -550,6 +570,46 @@ export declare function arrayUnion(...elements: unknown[]): FieldValue; The `FieldValue` sentinel for use in a call to `setDoc()` or `updateDoc()`. +## average() + +Create an AggregateField object that can be used to compute the average of a specified field over a range of documents in the result set of a query. + +Signature: + +```typescript +export declare function average(field: string | FieldPath): AggregateField; +``` + +### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| field | string \| [FieldPath](./firestore_lite.fieldpath.md#fieldpath_class) | Specifies the field to average across the result set. | + +Returns: + +[AggregateField](./firestore_lite.aggregatefield.md#aggregatefield_class)<number \| null> + +## sum() + +Create an AggregateField object that can be used to compute the sum of a specified field over a range of documents in the result set of a query. + +Signature: + +```typescript +export declare function sum(field: string | FieldPath): AggregateField; +``` + +### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| field | string \| [FieldPath](./firestore_lite.fieldpath.md#fieldpath_class) | Specifies the field to sum across the result set. | + +Returns: + +[AggregateField](./firestore_lite.aggregatefield.md#aggregatefield_class)<number> + ## orderBy() Creates a [QueryOrderByConstraint](./firestore_.queryorderbyconstraint.md#queryorderbyconstraint_class) that sorts the query result by the specified field, optionally in descending order instead of ascending. @@ -687,6 +747,27 @@ export declare function startAt(...fieldValues: unknown[]): QueryStartAtConstrai A [QueryStartAtConstraint](./firestore_.querystartatconstraint.md#querystartatconstraint_class) to pass to `query()`. +## aggregateFieldEqual() + +Compares two 'AggregateField\` instances for equality. + +Signature: + +```typescript +export declare function aggregateFieldEqual(left: AggregateField, right: AggregateField): boolean; +``` + +### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| left | [AggregateField](./firestore_lite.aggregatefield.md#aggregatefield_class)<unknown> | Compare this AggregateField to the right. | +| right | [AggregateField](./firestore_lite.aggregatefield.md#aggregatefield_class)<unknown> | Compare this AggregateField to the left. | + +Returns: + +boolean + ## aggregateQuerySnapshotEqual() Compares two `AggregateQuerySnapshot` instances for equality. @@ -873,6 +954,45 @@ export declare function increment(n: number): FieldValue; The `FieldValue` sentinel for use in a call to `setDoc()` or `updateDoc()` +## getAggregate() + +Calculates the specified aggregations over the documents in the result set of the given query, without actually downloading the documents. + +Using this function to perform aggregations is efficient because only the final aggregation values, not the documents' data, are downloaded. This function can even perform aggregations of the documents if the result set would be prohibitively large to download entirely (e.g. thousands of documents). + +Signature: + +```typescript +export declare function getAggregate(query: Query, aggregateSpec: AggregateSpecType): Promise>; +``` + +### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| query | [Query](./firestore_lite.query.md#query_class)<AppModelType, DbModelType> | The query whose result set to aggregate over. | +| aggregateSpec | AggregateSpecType | An AggregateSpec object that specifies the aggregates to perform over the result set. The AggregateSpec specifies aliases for each aggregate, which can be used to retrieve the aggregate result. | + +Returns: + +Promise<[AggregateQuerySnapshot](./firestore_lite.aggregatequerysnapshot.md#aggregatequerysnapshot_class)<AggregateSpecType, AppModelType, DbModelType>> + +### Example + + +```typescript +const aggregateSnapshot = await getAggregate(query, { + countOfDocs: count(), + totalHours: sum('hours'), + averageScore: average('score') +}); + +const countOfDocs: number = aggregateSnapshot.data().countOfDocs; +const totalHours: number = aggregateSnapshot.data().totalHours; +const averageScore: number | null = aggregateSnapshot.data().averageScore; + +``` + ## getCount() Calculates the number of documents in the result set of the given query, without actually downloading the documents. @@ -1435,7 +1555,7 @@ The union of all `AggregateField` types that are supported by Firestore. Signature: ```typescript -export declare type AggregateFieldType = AggregateField; +export declare type AggregateFieldType = ReturnType | ReturnType | ReturnType; ``` ## AggregateSpecData @@ -1450,6 +1570,16 @@ export declare type AggregateSpecData = { }; ``` +## AggregateType + +Union type representing the aggregate type to be performed. + +Signature: + +```typescript +export declare type AggregateType = 'count' | 'avg' | 'sum'; +``` + ## ChildUpdateFields Helper for calculating the nested fields for a given type T1. This is needed to distribute union types such as `undefined | {...}` (happens for optional props) or `{a: A} | {b: B}`. diff --git a/packages/firestore/src/api/aggregate.ts b/packages/firestore/src/api/aggregate.ts index 9628beac32c..57447b7ae09 100644 --- a/packages/firestore/src/api/aggregate.ts +++ b/packages/firestore/src/api/aggregate.ts @@ -80,7 +80,7 @@ export function getCountFromServer< * set of the given query, without actually downloading the documents. * * Using this function to perform aggregations is efficient because only the - * final aggregation values, not the documents' data, is downloaded. This + * final aggregation values, not the documents' data, are downloaded. This * function can even perform aggregations of the documents if the result set * would be prohibitively large to download entirely (e.g. thousands of documents). * @@ -107,7 +107,6 @@ export function getCountFromServer< * const totalHours: number = aggregateSnapshot.data().totalHours; * const averageScore: number | null = aggregateSnapshot.data().averageScore; * ``` - * @internal TODO (sum/avg) remove when public */ export function getAggregateFromServer< AggregateSpecType extends AggregateSpec, @@ -125,7 +124,7 @@ export function getAggregateFromServer< const internalAggregates = mapToArray(aggregateSpec, (aggregate, alias) => { return new AggregateImpl( alias, - aggregate._aggregateType, + aggregate.aggregateType, aggregate._internalFieldPath ); }); diff --git a/packages/firestore/src/core/aggregate.ts b/packages/firestore/src/core/aggregate.ts index 42cdd0524ff..3f69880bf2f 100644 --- a/packages/firestore/src/core/aggregate.ts +++ b/packages/firestore/src/core/aggregate.ts @@ -19,7 +19,6 @@ import { FieldPath } from '../model/path'; /** * Union type representing the aggregate type to be performed. - * @internal */ export type AggregateType = 'count' | 'avg' | 'sum'; diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index a36cf4fd5cd..fc51133f312 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -532,7 +532,6 @@ export function firestoreClientRunAggregateQuery( const deferred = new Deferred>(); client.asyncQueue.enqueueAndForget(async () => { - // TODO (sum/avg) should we update this to use the event manager? // Implement and call executeAggregateQueryViaSnapshotListener, similar // to the implementation in firestoreClientGetDocumentsViaSnapshotListener // above diff --git a/packages/firestore/src/lite-api/aggregate.ts b/packages/firestore/src/lite-api/aggregate.ts index 20cf8805c23..42b84ae01e7 100644 --- a/packages/firestore/src/lite-api/aggregate.ts +++ b/packages/firestore/src/lite-api/aggregate.ts @@ -70,7 +70,7 @@ export function getCount( * set of the given query, without actually downloading the documents. * * Using this function to perform aggregations is efficient because only the - * final aggregation values, not the documents' data, is downloaded. This + * final aggregation values, not the documents' data, are downloaded. This * function can even perform aggregations of the documents if the result set * would be prohibitively large to download entirely (e.g. thousands of documents). * @@ -90,7 +90,6 @@ export function getCount( * const totalHours: number = aggregateSnapshot.data().totalHours; * const averageScore: number | null = aggregateSnapshot.data().averageScore; * ``` - * @internal TODO (sum/avg) remove when public */ export function getAggregate< AggregateSpecType extends AggregateSpec, @@ -108,7 +107,7 @@ export function getAggregate< const internalAggregates = mapToArray(aggregateSpec, (aggregate, alias) => { return new AggregateImpl( alias, - aggregate._aggregateType, + aggregate.aggregateType, aggregate._internalFieldPath ); }); @@ -145,7 +144,6 @@ function convertToAggregateQuerySnapshot< * Create an AggregateField object that can be used to compute the sum of * a specified field over a range of documents in the result set of a query. * @param field Specifies the field to sum across the result set. - * @internal TODO (sum/avg) remove when public */ export function sum(field: string | FieldPath): AggregateField { return new AggregateField('sum', fieldPathFromArgument('sum', field)); @@ -155,7 +153,6 @@ export function sum(field: string | FieldPath): AggregateField { * Create an AggregateField object that can be used to compute the average of * a specified field over a range of documents in the result set of a query. * @param field Specifies the field to average across the result set. - * @internal TODO (sum/avg) remove when public */ export function average( field: string | FieldPath @@ -166,7 +163,6 @@ export function average( /** * Create an AggregateField object that can be used to compute the count of * documents in the result set of a query. - * @internal TODO (sum/avg) remove when public */ export function count(): AggregateField { return new AggregateField('count'); @@ -177,7 +173,6 @@ export function count(): AggregateField { * * @param left Compare this AggregateField to the `right`. * @param right Compare this AggregateField to the `left`. - * @internal TODO (sum/avg) remove when public */ export function aggregateFieldEqual( left: AggregateField, @@ -186,7 +181,7 @@ export function aggregateFieldEqual( return ( left instanceof AggregateField && right instanceof AggregateField && - left._aggregateType === right._aggregateType && + left.aggregateType === right.aggregateType && left._internalFieldPath?.canonicalString() === right._internalFieldPath?.canonicalString() ); diff --git a/packages/firestore/src/lite-api/aggregate_types.ts b/packages/firestore/src/lite-api/aggregate_types.ts index 8546057cc09..a87b3ca695d 100644 --- a/packages/firestore/src/lite-api/aggregate_types.ts +++ b/packages/firestore/src/lite-api/aggregate_types.ts @@ -19,6 +19,7 @@ import { AggregateType } from '../core/aggregate'; import { FieldPath as InternalFieldPath } from '../model/path'; import { ApiClientObjectMap, Value } from '../protos/firestore_proto_api'; +import { average, count, sum } from './aggregate'; import { DocumentData, Query } from './reference'; import { AbstractUserDataWriter } from './user_data_writer'; @@ -32,25 +33,30 @@ export class AggregateField { /** A type string to uniquely identify instances of this class. */ readonly type = 'AggregateField'; + /** Indicates the aggregation operation of this AggregateField. */ + readonly aggregateType: AggregateType; + /** * Create a new AggregateField - * @param _aggregateType Specifies the type of aggregation operation to perform. + * @param aggregateType Specifies the type of aggregation operation to perform. * @param _internalFieldPath Optionally specifies the field that is aggregated. * @internal */ constructor( - // TODO (sum/avg) make aggregateType public when the feature is supported - readonly _aggregateType: AggregateType = 'count', + aggregateType: AggregateType = 'count', readonly _internalFieldPath?: InternalFieldPath - ) {} + ) { + this.aggregateType = aggregateType; + } } -// TODO (sum/avg) Update the definition of AggregateFieldType to be based -// on the return type of `sum(..)`, `average(...)`, and `count()` /** * The union of all `AggregateField` types that are supported by Firestore. */ -export type AggregateFieldType = AggregateField; +export type AggregateFieldType = + | ReturnType + | ReturnType + | ReturnType; /** * Specifies a set of aggregations and their aliases. diff --git a/packages/firestore/test/integration/api/aggregation.test.ts b/packages/firestore/test/integration/api/aggregation.test.ts index 657f7f3688d..1316ad5c2ea 100644 --- a/packages/firestore/test/integration/api/aggregation.test.ts +++ b/packages/firestore/test/integration/api/aggregation.test.ts @@ -22,6 +22,7 @@ import { collectionGroup, disableNetwork, doc, + orderBy, DocumentData, getCountFromServer, getAggregateFromServer, @@ -369,17 +370,19 @@ apiDescribe('Aggregation queries', persistence => { ); }); -// TODO (sum/avg) enable these tests when sum/avg is supported by the backend -apiDescribe.skip('Aggregation queries - sum / average', persistence => { +apiDescribe('Aggregation queries - sum / average', persistence => { it('can run sum query getAggregationFromServer', () => { const testDocs = { a: { author: 'authorA', title: 'titleA', pages: 100 }, b: { author: 'authorB', title: 'titleB', pages: 50 } }; return withTestCollection(persistence, testDocs, async coll => { - const snapshot = await getAggregateFromServer(coll, { - totalPages: sum('pages') - }); + const snapshot = await getAggregateFromServer( + query(coll, orderBy('pages')), + { + totalPages: sum('pages') + } + ); expect(snapshot.data().totalPages).to.equal(150); }); }); @@ -475,57 +478,7 @@ apiDescribe.skip('Aggregation queries - sum / average', persistence => { }); }); - it('aggregate query supports collection groups', () => { - return withTestDb(persistence, async db => { - const collectionGroupId = doc(collection(db, 'aggregateQueryTest')).id; - const docPaths = [ - `${collectionGroupId}/cg-doc1`, - `abc/123/${collectionGroupId}/cg-doc2`, - `zzz${collectionGroupId}/cg-doc3`, - `abc/123/zzz${collectionGroupId}/cg-doc4`, - `abc/123/zzz/${collectionGroupId}` - ]; - const batch = writeBatch(db); - for (const docPath of docPaths) { - batch.set(doc(db, docPath), { x: 2 }); - } - await batch.commit(); - const snapshot = await getAggregateFromServer( - collectionGroup(db, collectionGroupId), - { - count: count(), - sum: sum('x'), - avg: average('x') - } - ); - expect(snapshot.data().count).to.equal(2); - expect(snapshot.data().sum).to.equal(4); - expect(snapshot.data().avg).to.equal(2); - }); - }); - - it('performs aggregations on documents with all aggregated fields using getAggregationFromServer', () => { - const testDocs = { - a: { author: 'authorA', title: 'titleA', pages: 100, year: 1980 }, - b: { author: 'authorB', title: 'titleB', pages: 50, year: 2020 }, - c: { author: 'authorC', title: 'titleC', pages: 150, year: 2021 }, - d: { author: 'authorD', title: 'titleD', pages: 50 } - }; - return withTestCollection(persistence, testDocs, async coll => { - const snapshot = await getAggregateFromServer(coll, { - totalPages: sum('pages'), - averagePages: average('pages'), - averageYear: average('year'), - count: count() - }); - expect(snapshot.data().totalPages).to.equal(300); - expect(snapshot.data().averagePages).to.equal(100); - expect(snapshot.data().averageYear).to.equal(2007); - expect(snapshot.data().count).to.equal(3); - }); - }); - - it('performs aggregates on multiple fields where one aggregate could cause short-circuit due to NaN using getAggregationFromServer', () => { + it('returns undefined when getting the result of an unrequested aggregation', () => { const testDocs = { a: { author: 'authorA', @@ -546,7 +499,7 @@ apiDescribe.skip('Aggregation queries - sum / average', persistence => { title: 'titleC', pages: 100, year: 1980, - rating: Number.NaN + rating: 3 }, d: { author: 'authorD', @@ -559,54 +512,8 @@ apiDescribe.skip('Aggregation queries - sum / average', persistence => { return withTestCollection(persistence, testDocs, async coll => { const snapshot = await getAggregateFromServer(coll, { totalRating: sum('rating'), - totalPages: sum('pages'), - averageYear: average('year') + averageRating: average('rating') }); - expect(snapshot.data().totalRating).to.be.NaN; - expect(snapshot.data().totalPages).to.equal(300); - expect(snapshot.data().averageYear).to.equal(2000); - }); - }); - - it('returns undefined when getting the result of an unrequested aggregation', () => { - const testDocs = { - a: { - author: 'authorA', - title: 'titleA', - pages: 100, - year: 1980, - rating: 5 - }, - b: { - author: 'authorB', - title: 'titleB', - pages: 50, - year: 2020, - rating: 4 - }, - c: { - author: 'authorC', - title: 'titleC', - pages: 100, - year: 1980, - rating: 3 - }, - d: { - author: 'authorD', - title: 'titleD', - pages: 50, - year: 2020, - rating: 0 - } - }; - return withTestCollection(persistence, testDocs, async coll => { - const snapshot = await getAggregateFromServer( - query(coll, where('pages', '>', 200)), - { - totalRating: sum('rating'), - averageRating: average('rating') - } - ); // @ts-expect-error const totalPages = snapshot.data().totalPages; @@ -651,65 +558,11 @@ apiDescribe.skip('Aggregation queries - sum / average', persistence => { { totalRating: sum('rating'), averageRating: average('rating'), - totalPages: sum('pages'), - averagePages: average('pages'), countOfDocs: count() } ); expect(snapshot.data().totalRating).to.equal(8); expect(snapshot.data().averageRating).to.equal(4); - expect(snapshot.data().totalPages).to.equal(200); - expect(snapshot.data().averagePages).to.equal(100); - expect(snapshot.data().countOfDocs).to.equal(2); - }); - }); - - it('performs aggregates when using `array-contains-any` operator getAggregationFromServer', () => { - const testDocs = { - a: { - author: 'authorA', - title: 'titleA', - pages: 100, - year: 1980, - rating: [5, 1000] - }, - b: { - author: 'authorB', - title: 'titleB', - pages: 50, - year: 2020, - rating: [4] - }, - c: { - author: 'authorC', - title: 'titleC', - pages: 100, - year: 1980, - rating: [2222, 3] - }, - d: { - author: 'authorD', - title: 'titleD', - pages: 50, - year: 2020, - rating: [0] - } - }; - return withTestCollection(persistence, testDocs, async coll => { - const snapshot = await getAggregateFromServer( - query(coll, where('rating', 'array-contains-any', [5, 3])), - { - totalRating: sum('rating'), - averageRating: average('rating'), - totalPages: sum('pages'), - averagePages: average('pages'), - countOfDocs: count() - } - ); - expect(snapshot.data().totalRating).to.equal(0); - expect(snapshot.data().averageRating).to.be.null; - expect(snapshot.data().totalPages).to.equal(200); - expect(snapshot.data().averagePages).to.equal(100); expect(snapshot.data().countOfDocs).to.equal(2); }); }); @@ -731,14 +584,10 @@ apiDescribe.skip('Aggregation queries - sum / average', persistence => { const snapshot = await getAggregateFromServer(coll, { totalPages: sum('metadata.pages'), averagePages: average('metadata.pages'), - averageCriticRating: average('metadata.rating.critic'), - totalUserRating: sum('metadata.rating.user'), count: count() }); expect(snapshot.data().totalPages).to.equal(150); expect(snapshot.data().averagePages).to.equal(75); - expect(snapshot.data().averageCriticRating).to.equal(3); - expect(snapshot.data().totalUserRating).to.equal(9); expect(snapshot.data().count).to.equal(2); }); }); @@ -986,6 +835,7 @@ apiDescribe.skip('Aggregation queries - sum / average', persistence => { }); it('performs sum that is valid but could overflow during aggregation using getAggregationFromServer', () => { + // Sum of rating would be 0, but if the accumulation overflow, we expect infinity const testDocs = { a: { author: 'authorA', @@ -1014,41 +864,17 @@ apiDescribe.skip('Aggregation queries - sum / average', persistence => { pages: 50, year: 2020, rating: -Number.MAX_VALUE - }, - e: { - author: 'authorE', - title: 'titleE', - pages: 100, - year: 1980, - rating: Number.MAX_VALUE - }, - f: { - author: 'authorF', - title: 'titleF', - pages: 50, - year: 2020, - rating: -Number.MAX_VALUE - }, - g: { - author: 'authorG', - title: 'titleG', - pages: 100, - year: 1980, - rating: -Number.MAX_VALUE - }, - h: { - author: 'authorH', - title: 'titleDH', - pages: 50, - year: 2020, - rating: Number.MAX_VALUE } }; return withTestCollection(persistence, testDocs, async coll => { const snapshot = await getAggregateFromServer(coll, { totalRating: sum('rating') }); - expect(snapshot.data().totalRating).to.equal(0); + expect(snapshot.data().totalRating).to.oneOf([ + Number.POSITIVE_INFINITY, + Number.NEGATIVE_INFINITY, + 0 + ]); }); }); @@ -1126,10 +952,10 @@ apiDescribe.skip('Aggregation queries - sum / average', persistence => { const snapshot = await getAggregateFromServer( query(coll, where('pages', '>', 200)), { - totalRating: sum('rating') + totalPages: sum('pages') } ); - expect(snapshot.data().totalRating).to.equal(0); + expect(snapshot.data().totalPages).to.equal(0); }); }); @@ -1512,10 +1338,10 @@ apiDescribe.skip('Aggregation queries - sum / average', persistence => { const snapshot = await getAggregateFromServer( query(coll, where('pages', '>', 200)), { - averageRating: average('rating') + averagePages: average('pages') } ); - expect(snapshot.data().averageRating).to.be.null; + expect(snapshot.data().averagePages).to.be.null; }); }); @@ -1559,4 +1385,156 @@ apiDescribe.skip('Aggregation queries - sum / average', persistence => { expect(snapshot.data().countOfDocs).to.equal(4); }); }); + + // Only run tests that require indexes against the emulator, because we don't + // have a way to dynamically create the indexes when running the tests. + (USE_EMULATOR ? apiDescribe : apiDescribe.skip)( + 'queries requiring indexes', + () => { + it('aggregate query supports collection groups - multi-aggregate', () => { + return withTestDb(persistence, async db => { + const collectionGroupId = doc( + collection(db, 'aggregateQueryTest') + ).id; + const docPaths = [ + `${collectionGroupId}/cg-doc1`, + `abc/123/${collectionGroupId}/cg-doc2`, + `zzz${collectionGroupId}/cg-doc3`, + `abc/123/zzz${collectionGroupId}/cg-doc4`, + `abc/123/zzz/${collectionGroupId}` + ]; + const batch = writeBatch(db); + for (const docPath of docPaths) { + batch.set(doc(db, docPath), { x: 2 }); + } + await batch.commit(); + const snapshot = await getAggregateFromServer( + collectionGroup(db, collectionGroupId), + { + count: count(), + sum: sum('x'), + avg: average('x') + } + ); + expect(snapshot.data().count).to.equal(2); + expect(snapshot.data().sum).to.equal(4); + expect(snapshot.data().avg).to.equal(2); + }); + }); + + it('performs aggregations on documents with all aggregated fields using getAggregationFromServer', () => { + const testDocs = { + a: { author: 'authorA', title: 'titleA', pages: 100, year: 1980 }, + b: { author: 'authorB', title: 'titleB', pages: 50, year: 2020 }, + c: { author: 'authorC', title: 'titleC', pages: 150, year: 2021 }, + d: { author: 'authorD', title: 'titleD', pages: 50 } + }; + return withTestCollection(persistence, testDocs, async coll => { + const snapshot = await getAggregateFromServer(coll, { + totalPages: sum('pages'), + averagePages: average('pages'), + averageYear: average('year'), + count: count() + }); + expect(snapshot.data().totalPages).to.equal(300); + expect(snapshot.data().averagePages).to.equal(100); + expect(snapshot.data().averageYear).to.equal(2007); + expect(snapshot.data().count).to.equal(3); + }); + }); + + it('performs aggregates on multiple fields where one aggregate could cause short-circuit due to NaN using getAggregationFromServer', () => { + const testDocs = { + a: { + author: 'authorA', + title: 'titleA', + pages: 100, + year: 1980, + rating: 5 + }, + b: { + author: 'authorB', + title: 'titleB', + pages: 50, + year: 2020, + rating: 4 + }, + c: { + author: 'authorC', + title: 'titleC', + pages: 100, + year: 1980, + rating: Number.NaN + }, + d: { + author: 'authorD', + title: 'titleD', + pages: 50, + year: 2020, + rating: 0 + } + }; + return withTestCollection(persistence, testDocs, async coll => { + const snapshot = await getAggregateFromServer(coll, { + totalRating: sum('rating'), + totalPages: sum('pages'), + averageYear: average('year') + }); + expect(snapshot.data().totalRating).to.be.NaN; + expect(snapshot.data().totalPages).to.equal(300); + expect(snapshot.data().averageYear).to.equal(2000); + }); + }); + + it('performs aggregates when using `array-contains-any` operator getAggregationFromServer', () => { + const testDocs = { + a: { + author: 'authorA', + title: 'titleA', + pages: 100, + year: 1980, + rating: [5, 1000] + }, + b: { + author: 'authorB', + title: 'titleB', + pages: 50, + year: 2020, + rating: [4] + }, + c: { + author: 'authorC', + title: 'titleC', + pages: 100, + year: 1980, + rating: [2222, 3] + }, + d: { + author: 'authorD', + title: 'titleD', + pages: 50, + year: 2020, + rating: [0] + } + }; + return withTestCollection(persistence, testDocs, async coll => { + const snapshot = await getAggregateFromServer( + query(coll, where('rating', 'array-contains-any', [5, 3])), + { + totalRating: sum('rating'), + averageRating: average('rating'), + totalPages: sum('pages'), + averagePages: average('pages'), + countOfDocs: count() + } + ); + expect(snapshot.data().totalRating).to.equal(0); + expect(snapshot.data().averageRating).to.be.null; + expect(snapshot.data().totalPages).to.equal(200); + expect(snapshot.data().averagePages).to.equal(100); + expect(snapshot.data().countOfDocs).to.equal(2); + }); + }); + } + ); }); diff --git a/packages/firestore/test/lite/integration.test.ts b/packages/firestore/test/lite/integration.test.ts index e86fdb9f6aa..6d1e1393fa6 100644 --- a/packages/firestore/test/lite/integration.test.ts +++ b/packages/firestore/test/lite/integration.test.ts @@ -2731,8 +2731,7 @@ describe('Aggregate queries', () => { ); }); -// TODO (sum/avg) enable these tests when sum/avg is supported by the backend -apiDescribe.skip('Aggregation queries - sum / average', () => { +describe('Aggregate queries - sum / average', () => { it('aggregateQuerySnapshotEqual on different aggregations to be falsy', () => { const testDocs = [ { author: 'authorA', title: 'titleA', rating: 1 }, @@ -2741,9 +2740,8 @@ apiDescribe.skip('Aggregation queries - sum / average', () => { { author: 'authorB', title: 'titleD', rating: 3 } ]; return withTestCollectionAndInitialData(testDocs, async coll => { - const query1 = query(coll, where('author', '==', 'authorA')); - const snapshot1 = await getAggregate(query1, { sum: sum('rating') }); - const snapshot2 = await getAggregate(query1, { avg: average('rating') }); + const snapshot1 = await getAggregate(coll, { sum: sum('rating') }); + const snapshot2 = await getAggregate(coll, { avg: average('rating') }); // `snapshot1` and `snapshot2` have different types and therefore the // following use of `aggregateQuerySnapshotEqual(...)` will cause a @@ -2761,9 +2759,8 @@ apiDescribe.skip('Aggregation queries - sum / average', () => { { author: 'authorB', title: 'titleD', rating: 3 } ]; return withTestCollectionAndInitialData(testDocs, async coll => { - const query1 = query(coll, where('author', '==', 'authorA')); - const snapshot1 = await getAggregate(query1, { foo: average('rating') }); - const snapshot2 = await getAggregate(query1, { bar: average('rating') }); + const snapshot1 = await getAggregate(coll, { foo: average('rating') }); + const snapshot2 = await getAggregate(coll, { bar: average('rating') }); // `snapshot1` and `snapshot2` have different types and therefore the // following use of `aggregateQuerySnapshotEqual(...)` will cause a @@ -2781,12 +2778,11 @@ apiDescribe.skip('Aggregation queries - sum / average', () => { { author: 'authorB', title: 'titleD', rating: 3 } ]; return withTestCollectionAndInitialData(testDocs, async coll => { - const query1 = query(coll, where('author', '==', 'authorA')); - const snapshot1 = await getAggregate(query1, { + const snapshot1 = await getAggregate(coll, { foo: average('rating'), bar: sum('rating') }); - const snapshot2 = await getAggregate(query1, { + const snapshot2 = await getAggregate(coll, { bar: sum('rating'), foo: average('rating') }); @@ -2859,24 +2855,31 @@ apiDescribe.skip('Aggregation queries - sum / average', () => { }); }); - it('performs aggregations on documents with all aggregated fields using getAggregationFromServer', () => { - const testDocs = [ - { author: 'authorA', title: 'titleA', pages: 100, year: 1980 }, - { author: 'authorB', title: 'titleB', pages: 50, year: 2020 }, - { author: 'authorC', title: 'titleC', pages: 150, year: 2021 }, - { author: 'authorD', title: 'titleD', pages: 50 } - ]; - return withTestCollectionAndInitialData(testDocs, async coll => { - const snapshot = await getAggregate(coll, { - totalPages: sum('pages'), - averagePages: average('pages'), - averageYear: average('year'), - count: count() + // Only run tests that require indexes against the emulator, because we don't + // have a way to dynamically create the indexes when running the tests. + (USE_EMULATOR ? apiDescribe : apiDescribe.skip)( + 'queries requiring indexes', + () => { + it('performs aggregations on documents with all aggregated fields using getAggregationFromServer', () => { + const testDocs = [ + { author: 'authorA', title: 'titleA', pages: 100, year: 1980 }, + { author: 'authorB', title: 'titleB', pages: 50, year: 2020 }, + { author: 'authorC', title: 'titleC', pages: 150, year: 2021 }, + { author: 'authorD', title: 'titleD', pages: 50 } + ]; + return withTestCollectionAndInitialData(testDocs, async coll => { + const snapshot = await getAggregate(coll, { + totalPages: sum('pages'), + averagePages: average('pages'), + averageYear: average('year'), + count: count() + }); + expect(snapshot.data().totalPages).to.equal(300); + expect(snapshot.data().averagePages).to.equal(100); + expect(snapshot.data().averageYear).to.equal(2007); + expect(snapshot.data().count).to.equal(3); + }); }); - expect(snapshot.data().totalPages).to.equal(300); - expect(snapshot.data().averagePages).to.equal(100); - expect(snapshot.data().averageYear).to.equal(2007); - expect(snapshot.data().count).to.equal(3); - }); - }); + } + ); });