Skip to content
This repository was archived by the owner on Mar 4, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions api-report/firestore.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Expression | unknown[]>): FunctionExpression;

Expand Down Expand Up @@ -958,6 +970,8 @@ export class ExplainResults<T> implements firestore.ExplainResults<T> {
abstract class Expression implements firestore.Pipelines.Expression, HasUserData {
abs(): FunctionExpression;
add(second: firestore.Pipelines.Expression | unknown, ...others: Array<firestore.Pipelines.Expression | unknown>): FunctionExpression;
arrayAgg(): AggregateFunction;
arrayAggDistinct(): AggregateFunction;
arrayConcat(secondArray: Expression | unknown[], ...otherArrays: Array<Expression | unknown[]>): FunctionExpression;
arrayContains(expression: Expression): BooleanExpression;
arrayContains(value: unknown): BooleanExpression;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -1906,6 +1934,10 @@ declare namespace Pipelines {
Constant,
sum,
maximum,
first,
last,
arrayAgg,
arrayAggDistinct,
descending,
greaterThanOrEqual,
multiply,
Expand Down
230 changes: 230 additions & 0 deletions dev/src/pipelines/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions dev/src/pipelines/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ export {
Constant,
sum,
maximum,
first,
last,
arrayAgg,
arrayAggDistinct,
descending,
greaterThanOrEqual,
multiply,
Expand Down
49 changes: 49 additions & 0 deletions dev/system-test/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ import {
multiply,
sum,
maximum,
first,
last,
arrayAgg,
arrayAggDistinct,
descending,
FunctionExpression,
minimum,
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading