diff --git a/api-report/firestore.api.md b/api-report/firestore.api.md index 73e85878b..193aec259 100644 --- a/api-report/firestore.api.md +++ b/api-report/firestore.api.md @@ -169,6 +169,18 @@ function and(first: BooleanExpression, second: BooleanExpression, ...more: Boole // @beta function array(elements: unknown[]): FunctionExpression; +// @beta +function arrayAgg(expression: Expression): AggregateFunction; + +// @beta +function arrayAgg(fieldName: string): AggregateFunction; + +// @beta +function arrayAggDistinct(expression: Expression): AggregateFunction; + +// @beta +function arrayAggDistinct(fieldName: string): AggregateFunction; + // @beta function arrayConcat(firstArray: Expression, secondArray: Expression | unknown[], ...otherArrays: Array): FunctionExpression; @@ -958,6 +970,8 @@ export class ExplainResults implements firestore.ExplainResults { abstract class Expression implements firestore.Pipelines.Expression, HasUserData { abs(): FunctionExpression; add(second: firestore.Pipelines.Expression | unknown, ...others: Array): FunctionExpression; + arrayAgg(): AggregateFunction; + arrayAggDistinct(): AggregateFunction; arrayConcat(secondArray: Expression | unknown[], ...otherArrays: Array): FunctionExpression; arrayContains(expression: Expression): BooleanExpression; arrayContains(value: unknown): BooleanExpression; @@ -1003,6 +1017,7 @@ abstract class Expression implements firestore.Pipelines.Expression, HasUserData exp(): FunctionExpression; // (undocumented) abstract expressionType: firestore.Pipelines.ExpressionType; + first(): AggregateFunction; floor(): FunctionExpression; greaterThan(expression: Expression): BooleanExpression; greaterThan(value: unknown): BooleanExpression; @@ -1017,6 +1032,7 @@ abstract class Expression implements firestore.Pipelines.Expression, HasUserData isType(type: Type): BooleanExpression; join(delimiterExpression: Expression): Expression; join(delimiter: string): Expression; + last(): AggregateFunction; length(): FunctionExpression; lessThan(experession: Expression): BooleanExpression; lessThan(value: unknown): BooleanExpression; @@ -1401,6 +1417,12 @@ class Firestore implements firestore.Firestore { export { Firestore } export default Firestore; +// @beta +function first(expression: Expression): AggregateFunction; + +// @beta +function first(fieldName: string): AggregateFunction; + // @beta function floor(expr: Expression): FunctionExpression; @@ -1527,6 +1549,12 @@ function join(arrayExpression: Expression, delimiter: string): Expression; // @beta function join(arrayFieldName: string, delimiterExpression: Expression): Expression; +// @beta +function last(expression: Expression): AggregateFunction; + +// @beta +function last(fieldName: string): AggregateFunction; + // @beta function length_2(fieldName: string): FunctionExpression; @@ -1906,6 +1934,10 @@ declare namespace Pipelines { Constant, sum, maximum, + first, + last, + arrayAgg, + arrayAggDistinct, descending, greaterThanOrEqual, multiply, diff --git a/dev/src/pipelines/expression.ts b/dev/src/pipelines/expression.ts index 01ee1e4fc..fb0e2417e 100644 --- a/dev/src/pipelines/expression.ts +++ b/dev/src/pipelines/expression.ts @@ -1484,6 +1484,80 @@ export abstract class Expression return new AggregateFunction('maximum', [this]); } + /** + * @beta + * Creates an aggregation that finds the first value of an expression across multiple stage inputs. + * + * @example + * ```typescript + * // Find the first value of the 'rating' field + * field("rating").first().as("firstRating"); + * ``` + * + * @returns A new `AggregateFunction` representing the 'first' aggregation. + */ + first(): AggregateFunction { + return new AggregateFunction('first', [this]); + } + + /** + * @beta + * Creates an aggregation that finds the last value of an expression across multiple stage inputs. + * + * @example + * ```typescript + * // Find the last value of the 'rating' field + * field("rating").last().as("lastRating"); + * ``` + * + * @returns A new `AggregateFunction` representing the 'last' aggregation. + */ + last(): AggregateFunction { + return new AggregateFunction('last', [this]); + } + + /** + * @beta + * Creates an aggregation that collects all values of an expression across multiple stage inputs + * into an array. + * + * @remarks + * If the expression resolves to an absent value, it is converted to `null`. + * The order of elements in the output array is not stable and shouldn't be relied upon. + * + * @example + * ```typescript + * // Collect all tags from books into an array + * field("tags").arrayAgg().as("allTags"); + * ``` + * + * @returns A new `AggregateFunction` representing the 'array_agg' aggregation. + */ + arrayAgg(): AggregateFunction { + return new AggregateFunction('array_agg', [this]); + } + + /** + * @beta + * Creates an aggregation that collects all distinct values of an expression across multiple stage + * inputs into an array. + * + * @remarks + * If the expression resolves to an absent value, it is converted to `null`. + * The order of elements in the output array is not stable and shouldn't be relied upon. + * + * @example + * ```typescript + * // Collect all distinct tags from books into an array + * field("tags").arrayAggDistinct().as("allDistinctTags"); + * ``` + * + * @returns A new `AggregateFunction` representing the 'array_agg_distinct' aggregation. + */ + arrayAggDistinct(): AggregateFunction { + return new AggregateFunction('array_agg_distinct', [this]); + } + /** * @beta * Creates an aggregation that counts the number of distinct values of the expression or field. @@ -6915,6 +6989,162 @@ export function maximum(value: Expression | string): AggregateFunction { return fieldOrExpression(value).maximum(); } +/** + * @beta + * Creates an aggregation that finds the first value of an expression across multiple stage + * inputs. + * + * @example + * ```typescript + * // Find the first value of the 'rating' field + * first(field("rating")).as("firstRating"); + * ``` + * + * @param expression The expression to find the first value of. + * @returns A new `AggregateFunction` representing the 'first' aggregation. + */ +export function first(expression: Expression): AggregateFunction; + +/** + * @beta + * Creates an aggregation that finds the first value of a field across multiple stage inputs. + * + * @example + * ```typescript + * // Find the first value of the 'rating' field + * first("rating").as("firstRating"); + * ``` + * + * @param fieldName The name of the field to find the first value of. + * @returns A new `AggregateFunction` representing the 'first' aggregation. + */ +export function first(fieldName: string): AggregateFunction; +export function first(value: Expression | string): AggregateFunction { + return fieldOrExpression(value).first(); +} + +/** + * @beta + * Creates an aggregation that finds the last value of an expression across multiple stage + * inputs. + * + * @example + * ```typescript + * // Find the last value of the 'rating' field + * last(field("rating")).as("lastRating"); + * ``` + * + * @param expression The expression to find the last value of. + * @returns A new `AggregateFunction` representing the 'last' aggregation. + */ +export function last(expression: Expression): AggregateFunction; + +/** + * @beta + * Creates an aggregation that finds the last value of a field across multiple stage inputs. + * + * @example + * ```typescript + * // Find the last value of the 'rating' field + * last("rating").as("lastRating"); + * ``` + * + * @param fieldName The name of the field to find the last value of. + * @returns A new `AggregateFunction` representing the 'last' aggregation. + */ +export function last(fieldName: string): AggregateFunction; +export function last(value: Expression | string): AggregateFunction { + return fieldOrExpression(value).last(); +} + +/** + * @beta + * Creates an aggregation that collects all values of an expression across multiple stage + * inputs into an array. + * + * @remarks + * If the expression resolves to an absent value, it is converted to `null`. + * The order of elements in the output array is not stable and shouldn't be relied upon. + * + * @example + * ```typescript + * // Collect all tags from books into an array + * arrayAgg(field("tags")).as("allTags"); + * ``` + * + * @param expression The expression to collect values from. + * @returns A new `AggregateFunction` representing the 'array_agg' aggregation. + */ +export function arrayAgg(expression: Expression): AggregateFunction; + +/** + * @beta + * Creates an aggregation that collects all values of a field across multiple stage inputs + * into an array. + * + * @remarks + * If the expression resolves to an absent value, it is converted to `null`. + * The order of elements in the output array is not stable and shouldn't be relied upon. + * + * @example + * ```typescript + * // Collect all tags from books into an array + * arrayAgg("tags").as("allTags"); + * ``` + * + * @param fieldName The name of the field to collect values from. + * @returns A new `AggregateFunction` representing the 'array_agg' aggregation. + */ +export function arrayAgg(fieldName: string): AggregateFunction; +export function arrayAgg(value: Expression | string): AggregateFunction { + return fieldOrExpression(value).arrayAgg(); +} + +/** + * @beta + * Creates an aggregation that collects all distinct values of an expression across multiple stage + * inputs into an array. + * + * @remarks + * If the expression resolves to an absent value, it is converted to `null`. + * The order of elements in the output array is not stable and shouldn't be relied upon. + * + * @example + * ```typescript + * // Collect all distinct tags from books into an array + * arrayAggDistinct(field("tags")).as("allDistinctTags"); + * ``` + * + * @param expression The expression to collect values from. + * @returns A new `AggregateFunction` representing the 'array_agg_distinct' aggregation. + */ +export function arrayAggDistinct(expression: Expression): AggregateFunction; + +/** + * @beta + * Creates an aggregation that collects all distinct values of a field across multiple stage inputs + * into an array. + * + * @remarks + * If the expression resolves to an absent value, it is converted to `null`. + * The order of elements in the output array is not stable and shouldn't be relied upon. + * + * @example + * ```typescript + * // Collect all distinct tags from books into an array + * arrayAggDistinct("tags").as("allDistinctTags"); + * ``` + * + * @param fieldName The name of the field to collect values from. + * @returns A new `AggregateFunction` representing the 'array_agg_distinct' aggregation. + */ +export function arrayAggDistinct(fieldName: string): AggregateFunction; +export function arrayAggDistinct( + value: Expression | string, +): AggregateFunction { + return fieldOrExpression(value).arrayAggDistinct(); +} + /** * @beta * Calculates the Cosine distance between a field's vector value and a literal vector value. diff --git a/dev/src/pipelines/index.ts b/dev/src/pipelines/index.ts index 91333c94b..012a8fbc8 100644 --- a/dev/src/pipelines/index.ts +++ b/dev/src/pipelines/index.ts @@ -89,6 +89,10 @@ export { Constant, sum, maximum, + first, + last, + arrayAgg, + arrayAggDistinct, descending, greaterThanOrEqual, multiply, diff --git a/dev/system-test/pipeline.ts b/dev/system-test/pipeline.ts index 783d8280a..a584a9b14 100644 --- a/dev/system-test/pipeline.ts +++ b/dev/system-test/pipeline.ts @@ -38,6 +38,10 @@ import { multiply, sum, maximum, + first, + last, + arrayAgg, + arrayAggDistinct, descending, FunctionExpression, minimum, @@ -1039,6 +1043,51 @@ describe.skipClassic('Pipeline class', () => { }); }); + it('returns first and last accumulations', async () => { + const snapshot = await firestore + .pipeline() + .collection(randomCol.path) + .sort(field('published').ascending()) + .aggregate( + first('rating').as('firstBookRating'), + first('title').as('firstBookTitle'), + last('rating').as('lastBookRating'), + last('title').as('lastBookTitle'), + ) + .execute(); + expectResults(snapshot, { + firstBookRating: 4.5, + firstBookTitle: 'Pride and Prejudice', + lastBookRating: 4.1, + lastBookTitle: "The Handmaid's Tale", + }); + }); + + it('returns arrayAgg accumulations', async () => { + const snapshot = await firestore + .pipeline() + .collection(randomCol.path) + .sort(field('published').ascending()) + .aggregate(arrayAgg('rating').as('allRatings')) + .execute(); + expectResults(snapshot, { + allRatings: [4.5, 4.3, 4.0, 4.2, 4.7, 4.2, 4.6, 4.3, 4.2, 4.1], + }); + }); + + it('returns arrayAggDistinct accumulations', async () => { + const snapshot = await firestore + .pipeline() + .collection(randomCol.path) + .aggregate(arrayAggDistinct('rating').as('allDistinctRatings')) + .execute(); + const data = snapshot.results[0].data(); + data['allDistinctRatings'].sort((a: number, b: number) => a - b); + expect(data).to.deep.equal({ + allDistinctRatings: [4.0, 4.1, 4.2, 4.3, 4.5, 4.6, 4.7], + }); + }); + it('rejects groups without accumulators', async () => { void expect(async () => { await firestore diff --git a/types/firestore.d.ts b/types/firestore.d.ts index 2272224ed..7c2c44ea1 100644 --- a/types/firestore.d.ts +++ b/types/firestore.d.ts @@ -4276,6 +4276,68 @@ declare namespace FirebaseFirestore { * @returns A new `AggregateFunction` representing the 'max' aggregation. */ maximum(): AggregateFunction; + /** + * @beta + * Creates an aggregation that finds the first value of an expression across multiple stage inputs. + * + * @example + * ```typescript + * // Find the first value of the 'rating' field + * field("rating").first().as("firstRating"); + * ``` + * + * @returns A new `AggregateFunction` representing the 'first' aggregation. + */ + first(): AggregateFunction; + /** + * @beta + * Creates an aggregation that finds the last value of an expression across multiple stage inputs. + * + * @example + * ```typescript + * // Find the last value of the 'rating' field + * field("rating").last().as("lastRating"); + * ``` + * + * @returns A new `AggregateFunction` representing the 'last' aggregation. + */ + last(): AggregateFunction; + /** + * @beta + * Creates an aggregation that collects all values of an expression across multiple stage inputs + * into an array. + * + * @remarks + * If the expression resolves to an absent value, it is converted to `null`. + * The order of elements in the output array is not stable and shouldn't be relied upon. + * + * @example + * ```typescript + * // Collect all tags from books into an array + * field("tags").arrayAgg().as("allTags"); + * ``` + * + * @returns A new `AggregateFunction` representing the 'array_agg' aggregation. + */ + arrayAgg(): AggregateFunction; + /** + * @beta + * Creates an aggregation that collects all distinct values of an expression across multiple stage + * inputs into an array. + * + * @remarks + * If the expression resolves to an absent value, it is converted to `null`. + * The order of elements in the output array is not stable and shouldn't be relied upon. + * + * @example + * ```typescript + * // Collect all distinct tags from books into an array + * field("tags").arrayAggDistinct().as("allDistinctTags"); + * ``` + * + * @returns A new `AggregateFunction` representing the 'array_agg_distinct' aggregation. + */ + arrayAggDistinct(): AggregateFunction; /** * @beta * Creates an expression that returns the larger value between this expression and another expression, based on Firestore's value type ordering. @@ -8831,6 +8893,138 @@ declare namespace FirebaseFirestore { * @returns A new {@code AggregateFunction} representing the 'max' aggregation. */ export function maximum(fieldName: string): AggregateFunction; + /** + * @beta + * Creates an aggregation that finds the first value of an expression across multiple stage + * inputs. + * + * @example + * ```typescript + * // Find the first value of the 'rating' field + * first(field("rating")).as("firstRating"); + * ``` + * + * @param expression The expression to find the first value of. + * @returns A new {@code AggregateFunction} representing the 'first' aggregation. + */ + export function first(expression: Expression): AggregateFunction; + /** + * @beta + * Creates an aggregation that finds the first value of a field across multiple stage inputs. + * + * @example + * ```typescript + * // Find the first value of the 'rating' field + * first("rating").as("firstRating"); + * ``` + * + * @param fieldName The name of the field to find the first value of. + * @returns A new {@code AggregateFunction} representing the 'first' aggregation. + */ + export function first(fieldName: string): AggregateFunction; + /** + * @beta + * Creates an aggregation that finds the last value of an expression across multiple stage + * inputs. + * + * @example + * ```typescript + * // Find the last value of the 'rating' field + * last(field("rating")).as("lastRating"); + * ``` + * + * @param expression The expression to find the last value of. + * @returns A new {@code AggregateFunction} representing the 'last' aggregation. + */ + export function last(expression: Expression): AggregateFunction; + /** + * @beta + * Creates an aggregation that finds the last value of a field across multiple stage inputs. + * + * @example + * ```typescript + * // Find the last value of the 'rating' field + * last("rating").as("lastRating"); + * ``` + * + * @param fieldName The name of the field to find the last value of. + * @returns A new {@code AggregateFunction} representing the 'last' aggregation. + */ + export function last(fieldName: string): AggregateFunction; + /** + * @beta + * Creates an aggregation that collects all values of an expression across multiple stage + * inputs into an array. + * + * @remarks + * If the expression resolves to an absent value, it is converted to `null`. + * The order of elements in the output array is not stable and shouldn't be relied upon. + * + * @example + * ```typescript + * // Collect all tags from books into an array + * arrayAgg(field("tags")).as("allTags"); + * ``` + * + * @param expression The expression to collect values from. + * @returns A new {@code AggregateFunction} representing the 'array_agg' aggregation. + */ + export function arrayAgg(expression: Expression): AggregateFunction; + /** + * @beta + * Creates an aggregation that collects all values of a field across multiple stage inputs + * into an array. + * + * @remarks + * If the expression resolves to an absent value, it is converted to `null`. + * The order of elements in the output array is not stable and shouldn't be relied upon. + * + * @example + * ```typescript + * // Collect all tags from books into an array + * arrayAgg("tags").as("allTags"); + * ``` + * + * @param fieldName The name of the field to collect values from. + * @returns A new {@code AggregateFunction} representing the 'array_agg' aggregation. + */ + export function arrayAgg(fieldName: string): AggregateFunction; + /** + * @beta + * Creates an aggregation that collects all distinct values of an expression across multiple stage + * inputs into an array. + * + * @remarks + * If the expression resolves to an absent value, it is converted to `null`. + * The order of elements in the output array is not stable and shouldn't be relied upon. + * + * @example + * ```typescript + * // Collect all distinct tags from books into an array + * arrayAggDistinct(field("tags")).as("allDistinctTags"); + * ``` + * @param expression The expression to collect values from. + * @returns A new {@code AggregateFunction} representing the 'array_agg_distinct' aggregation. + */ + export function arrayAggDistinct(expression: Expression): AggregateFunction; + /** + * @beta + * Creates an aggregation that collects all distinct values of a field across multiple stage inputs + * into an array. + * + * @remarks + * If the expression resolves to an absent value, it is converted to `null`. + * The order of elements in the output array is not stable and shouldn't be relied upon. + * + * @example + * ```typescript + * // Collect all distinct tags from books into an array + * arrayAggDistinct("tags").as("allDistinctTags"); + * ``` + * @param fieldName The name of the field to collect values from. + * @returns A new {@code AggregateFunction} representing the 'array_agg_distinct' aggregation. + */ + export function arrayAggDistinct(fieldName: string): AggregateFunction; /** * @beta * Calculates the Cosine distance between a field's vector value and a literal vector value.