Skip to content
This repository was archived by the owner on Mar 4, 2026. It is now read-only.

Commit a1c66d1

Browse files
feat: add first, last, arrayAgg and arrayAggDistinct expressions (#2494)
* feat: add first, last, arrayAgg and arrayAggDistinct expressions * remove the only error for tests * remove the dash for doc consistency * replace the the function link with * replace the function link with aggregationFunction * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent 920c9f7 commit a1c66d1

5 files changed

Lines changed: 509 additions & 0 deletions

File tree

api-report/firestore.api.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,18 @@ function and(first: BooleanExpression, second: BooleanExpression, ...more: Boole
169169
// @beta
170170
function array(elements: unknown[]): FunctionExpression;
171171

172+
// @beta
173+
function arrayAgg(expression: Expression): AggregateFunction;
174+
175+
// @beta
176+
function arrayAgg(fieldName: string): AggregateFunction;
177+
178+
// @beta
179+
function arrayAggDistinct(expression: Expression): AggregateFunction;
180+
181+
// @beta
182+
function arrayAggDistinct(fieldName: string): AggregateFunction;
183+
172184
// @beta
173185
function arrayConcat(firstArray: Expression, secondArray: Expression | unknown[], ...otherArrays: Array<Expression | unknown[]>): FunctionExpression;
174186

@@ -958,6 +970,8 @@ export class ExplainResults<T> implements firestore.ExplainResults<T> {
958970
abstract class Expression implements firestore.Pipelines.Expression, HasUserData {
959971
abs(): FunctionExpression;
960972
add(second: firestore.Pipelines.Expression | unknown, ...others: Array<firestore.Pipelines.Expression | unknown>): FunctionExpression;
973+
arrayAgg(): AggregateFunction;
974+
arrayAggDistinct(): AggregateFunction;
961975
arrayConcat(secondArray: Expression | unknown[], ...otherArrays: Array<Expression | unknown[]>): FunctionExpression;
962976
arrayContains(expression: Expression): BooleanExpression;
963977
arrayContains(value: unknown): BooleanExpression;
@@ -1003,6 +1017,7 @@ abstract class Expression implements firestore.Pipelines.Expression, HasUserData
10031017
exp(): FunctionExpression;
10041018
// (undocumented)
10051019
abstract expressionType: firestore.Pipelines.ExpressionType;
1020+
first(): AggregateFunction;
10061021
floor(): FunctionExpression;
10071022
greaterThan(expression: Expression): BooleanExpression;
10081023
greaterThan(value: unknown): BooleanExpression;
@@ -1017,6 +1032,7 @@ abstract class Expression implements firestore.Pipelines.Expression, HasUserData
10171032
isType(type: Type): BooleanExpression;
10181033
join(delimiterExpression: Expression): Expression;
10191034
join(delimiter: string): Expression;
1035+
last(): AggregateFunction;
10201036
length(): FunctionExpression;
10211037
lessThan(experession: Expression): BooleanExpression;
10221038
lessThan(value: unknown): BooleanExpression;
@@ -1401,6 +1417,12 @@ class Firestore implements firestore.Firestore {
14011417
export { Firestore }
14021418
export default Firestore;
14031419

1420+
// @beta
1421+
function first(expression: Expression): AggregateFunction;
1422+
1423+
// @beta
1424+
function first(fieldName: string): AggregateFunction;
1425+
14041426
// @beta
14051427
function floor(expr: Expression): FunctionExpression;
14061428

@@ -1527,6 +1549,12 @@ function join(arrayExpression: Expression, delimiter: string): Expression;
15271549
// @beta
15281550
function join(arrayFieldName: string, delimiterExpression: Expression): Expression;
15291551

1552+
// @beta
1553+
function last(expression: Expression): AggregateFunction;
1554+
1555+
// @beta
1556+
function last(fieldName: string): AggregateFunction;
1557+
15301558
// @beta
15311559
function length_2(fieldName: string): FunctionExpression;
15321560

@@ -1906,6 +1934,10 @@ declare namespace Pipelines {
19061934
Constant,
19071935
sum,
19081936
maximum,
1937+
first,
1938+
last,
1939+
arrayAgg,
1940+
arrayAggDistinct,
19091941
descending,
19101942
greaterThanOrEqual,
19111943
multiply,

dev/src/pipelines/expression.ts

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1484,6 +1484,80 @@ export abstract class Expression
14841484
return new AggregateFunction('maximum', [this]);
14851485
}
14861486

1487+
/**
1488+
* @beta
1489+
* Creates an aggregation that finds the first value of an expression across multiple stage inputs.
1490+
*
1491+
* @example
1492+
* ```typescript
1493+
* // Find the first value of the 'rating' field
1494+
* field("rating").first().as("firstRating");
1495+
* ```
1496+
*
1497+
* @returns A new `AggregateFunction` representing the 'first' aggregation.
1498+
*/
1499+
first(): AggregateFunction {
1500+
return new AggregateFunction('first', [this]);
1501+
}
1502+
1503+
/**
1504+
* @beta
1505+
* Creates an aggregation that finds the last value of an expression across multiple stage inputs.
1506+
*
1507+
* @example
1508+
* ```typescript
1509+
* // Find the last value of the 'rating' field
1510+
* field("rating").last().as("lastRating");
1511+
* ```
1512+
*
1513+
* @returns A new `AggregateFunction` representing the 'last' aggregation.
1514+
*/
1515+
last(): AggregateFunction {
1516+
return new AggregateFunction('last', [this]);
1517+
}
1518+
1519+
/**
1520+
* @beta
1521+
* Creates an aggregation that collects all values of an expression across multiple stage inputs
1522+
* into an array.
1523+
*
1524+
* @remarks
1525+
* If the expression resolves to an absent value, it is converted to `null`.
1526+
* The order of elements in the output array is not stable and shouldn't be relied upon.
1527+
*
1528+
* @example
1529+
* ```typescript
1530+
* // Collect all tags from books into an array
1531+
* field("tags").arrayAgg().as("allTags");
1532+
* ```
1533+
*
1534+
* @returns A new `AggregateFunction` representing the 'array_agg' aggregation.
1535+
*/
1536+
arrayAgg(): AggregateFunction {
1537+
return new AggregateFunction('array_agg', [this]);
1538+
}
1539+
1540+
/**
1541+
* @beta
1542+
* Creates an aggregation that collects all distinct values of an expression across multiple stage
1543+
* inputs into an array.
1544+
*
1545+
* @remarks
1546+
* If the expression resolves to an absent value, it is converted to `null`.
1547+
* The order of elements in the output array is not stable and shouldn't be relied upon.
1548+
*
1549+
* @example
1550+
* ```typescript
1551+
* // Collect all distinct tags from books into an array
1552+
* field("tags").arrayAggDistinct().as("allDistinctTags");
1553+
* ```
1554+
*
1555+
* @returns A new `AggregateFunction` representing the 'array_agg_distinct' aggregation.
1556+
*/
1557+
arrayAggDistinct(): AggregateFunction {
1558+
return new AggregateFunction('array_agg_distinct', [this]);
1559+
}
1560+
14871561
/**
14881562
* @beta
14891563
* 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 {
69156989
return fieldOrExpression(value).maximum();
69166990
}
69176991

6992+
/**
6993+
* @beta
6994+
* Creates an aggregation that finds the first value of an expression across multiple stage
6995+
* inputs.
6996+
*
6997+
* @example
6998+
* ```typescript
6999+
* // Find the first value of the 'rating' field
7000+
* first(field("rating")).as("firstRating");
7001+
* ```
7002+
*
7003+
* @param expression The expression to find the first value of.
7004+
* @returns A new `AggregateFunction` representing the 'first' aggregation.
7005+
*/
7006+
export function first(expression: Expression): AggregateFunction;
7007+
7008+
/**
7009+
* @beta
7010+
* Creates an aggregation that finds the first value of a field across multiple stage inputs.
7011+
*
7012+
* @example
7013+
* ```typescript
7014+
* // Find the first value of the 'rating' field
7015+
* first("rating").as("firstRating");
7016+
* ```
7017+
*
7018+
* @param fieldName The name of the field to find the first value of.
7019+
* @returns A new `AggregateFunction` representing the 'first' aggregation.
7020+
*/
7021+
export function first(fieldName: string): AggregateFunction;
7022+
export function first(value: Expression | string): AggregateFunction {
7023+
return fieldOrExpression(value).first();
7024+
}
7025+
7026+
/**
7027+
* @beta
7028+
* Creates an aggregation that finds the last value of an expression across multiple stage
7029+
* inputs.
7030+
*
7031+
* @example
7032+
* ```typescript
7033+
* // Find the last value of the 'rating' field
7034+
* last(field("rating")).as("lastRating");
7035+
* ```
7036+
*
7037+
* @param expression The expression to find the last value of.
7038+
* @returns A new `AggregateFunction` representing the 'last' aggregation.
7039+
*/
7040+
export function last(expression: Expression): AggregateFunction;
7041+
7042+
/**
7043+
* @beta
7044+
* Creates an aggregation that finds the last value of a field across multiple stage inputs.
7045+
*
7046+
* @example
7047+
* ```typescript
7048+
* // Find the last value of the 'rating' field
7049+
* last("rating").as("lastRating");
7050+
* ```
7051+
*
7052+
* @param fieldName The name of the field to find the last value of.
7053+
* @returns A new `AggregateFunction` representing the 'last' aggregation.
7054+
*/
7055+
export function last(fieldName: string): AggregateFunction;
7056+
export function last(value: Expression | string): AggregateFunction {
7057+
return fieldOrExpression(value).last();
7058+
}
7059+
7060+
/**
7061+
* @beta
7062+
* Creates an aggregation that collects all values of an expression across multiple stage
7063+
* inputs into an array.
7064+
*
7065+
* @remarks
7066+
* If the expression resolves to an absent value, it is converted to `null`.
7067+
* The order of elements in the output array is not stable and shouldn't be relied upon.
7068+
*
7069+
* @example
7070+
* ```typescript
7071+
* // Collect all tags from books into an array
7072+
* arrayAgg(field("tags")).as("allTags");
7073+
* ```
7074+
*
7075+
* @param expression The expression to collect values from.
7076+
* @returns A new `AggregateFunction` representing the 'array_agg' aggregation.
7077+
*/
7078+
export function arrayAgg(expression: Expression): AggregateFunction;
7079+
7080+
/**
7081+
* @beta
7082+
* Creates an aggregation that collects all values of a field across multiple stage inputs
7083+
* into an array.
7084+
*
7085+
* @remarks
7086+
* If the expression resolves to an absent value, it is converted to `null`.
7087+
* The order of elements in the output array is not stable and shouldn't be relied upon.
7088+
*
7089+
* @example
7090+
* ```typescript
7091+
* // Collect all tags from books into an array
7092+
* arrayAgg("tags").as("allTags");
7093+
* ```
7094+
*
7095+
* @param fieldName The name of the field to collect values from.
7096+
* @returns A new `AggregateFunction` representing the 'array_agg' aggregation.
7097+
*/
7098+
export function arrayAgg(fieldName: string): AggregateFunction;
7099+
export function arrayAgg(value: Expression | string): AggregateFunction {
7100+
return fieldOrExpression(value).arrayAgg();
7101+
}
7102+
7103+
/**
7104+
* @beta
7105+
* Creates an aggregation that collects all distinct values of an expression across multiple stage
7106+
* inputs into an array.
7107+
*
7108+
* @remarks
7109+
* If the expression resolves to an absent value, it is converted to `null`.
7110+
* The order of elements in the output array is not stable and shouldn't be relied upon.
7111+
*
7112+
* @example
7113+
* ```typescript
7114+
* // Collect all distinct tags from books into an array
7115+
* arrayAggDistinct(field("tags")).as("allDistinctTags");
7116+
* ```
7117+
*
7118+
* @param expression The expression to collect values from.
7119+
* @returns A new `AggregateFunction` representing the 'array_agg_distinct' aggregation.
7120+
*/
7121+
export function arrayAggDistinct(expression: Expression): AggregateFunction;
7122+
7123+
/**
7124+
* @beta
7125+
* Creates an aggregation that collects all distinct values of a field across multiple stage inputs
7126+
* into an array.
7127+
*
7128+
* @remarks
7129+
* If the expression resolves to an absent value, it is converted to `null`.
7130+
* The order of elements in the output array is not stable and shouldn't be relied upon.
7131+
*
7132+
* @example
7133+
* ```typescript
7134+
* // Collect all distinct tags from books into an array
7135+
* arrayAggDistinct("tags").as("allDistinctTags");
7136+
* ```
7137+
*
7138+
* @param fieldName The name of the field to collect values from.
7139+
* @returns A new `AggregateFunction` representing the 'array_agg_distinct' aggregation.
7140+
*/
7141+
export function arrayAggDistinct(fieldName: string): AggregateFunction;
7142+
export function arrayAggDistinct(
7143+
value: Expression | string,
7144+
): AggregateFunction {
7145+
return fieldOrExpression(value).arrayAggDistinct();
7146+
}
7147+
69187148
/**
69197149
* @beta
69207150
* Calculates the Cosine distance between a field's vector value and a literal vector value.

dev/src/pipelines/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ export {
8989
Constant,
9090
sum,
9191
maximum,
92+
first,
93+
last,
94+
arrayAgg,
95+
arrayAggDistinct,
9296
descending,
9397
greaterThanOrEqual,
9498
multiply,

dev/system-test/pipeline.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ import {
3838
multiply,
3939
sum,
4040
maximum,
41+
first,
42+
last,
43+
arrayAgg,
44+
arrayAggDistinct,
4145
descending,
4246
FunctionExpression,
4347
minimum,
@@ -1039,6 +1043,51 @@ describe.skipClassic('Pipeline class', () => {
10391043
});
10401044
});
10411045

1046+
it('returns first and last accumulations', async () => {
1047+
const snapshot = await firestore
1048+
.pipeline()
1049+
.collection(randomCol.path)
1050+
.sort(field('published').ascending())
1051+
.aggregate(
1052+
first('rating').as('firstBookRating'),
1053+
first('title').as('firstBookTitle'),
1054+
last('rating').as('lastBookRating'),
1055+
last('title').as('lastBookTitle'),
1056+
)
1057+
.execute();
1058+
expectResults(snapshot, {
1059+
firstBookRating: 4.5,
1060+
firstBookTitle: 'Pride and Prejudice',
1061+
lastBookRating: 4.1,
1062+
lastBookTitle: "The Handmaid's Tale",
1063+
});
1064+
});
1065+
1066+
it('returns arrayAgg accumulations', async () => {
1067+
const snapshot = await firestore
1068+
.pipeline()
1069+
.collection(randomCol.path)
1070+
.sort(field('published').ascending())
1071+
.aggregate(arrayAgg('rating').as('allRatings'))
1072+
.execute();
1073+
expectResults(snapshot, {
1074+
allRatings: [4.5, 4.3, 4.0, 4.2, 4.7, 4.2, 4.6, 4.3, 4.2, 4.1],
1075+
});
1076+
});
1077+
1078+
it('returns arrayAggDistinct accumulations', async () => {
1079+
const snapshot = await firestore
1080+
.pipeline()
1081+
.collection(randomCol.path)
1082+
.aggregate(arrayAggDistinct('rating').as('allDistinctRatings'))
1083+
.execute();
1084+
const data = snapshot.results[0].data();
1085+
data['allDistinctRatings'].sort((a: number, b: number) => a - b);
1086+
expect(data).to.deep.equal({
1087+
allDistinctRatings: [4.0, 4.1, 4.2, 4.3, 4.5, 4.6, 4.7],
1088+
});
1089+
});
1090+
10421091
it('rejects groups without accumulators', async () => {
10431092
void expect(async () => {
10441093
await firestore

0 commit comments

Comments
 (0)