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

Commit 7fb04dc

Browse files
yvonnep165cloud-java-bot
authored andcommitted
feat: add first, last, arrayAgg and arrayAggDistinct expressions (#2334)
* add first, last, arrayAgg and arrayAggDistinct expressions and documentation * chore: generate libraries at Mon Mar 2 16:03:26 UTC 2026 * add instance method tests for the ported expression * remove code samples in PipelineSnippets.java --------- Co-authored-by: cloud-java-bot <cloud-java-bot@google.com>
1 parent fbd07e2 commit 7fb04dc

3 files changed

Lines changed: 285 additions & 0 deletions

File tree

google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/AggregateFunction.java

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,111 @@ public static AggregateFunction maximum(Expression expression) {
214214
return new AggregateFunction("maximum", expression);
215215
}
216216

217+
/**
218+
* Creates an aggregation that finds the first value of a field across multiple stage inputs.
219+
*
220+
* @param fieldName The name of the field to find the first value of.
221+
* @return A new {@link AggregateFunction} representing the first aggregation.
222+
*/
223+
@BetaApi
224+
public static AggregateFunction first(String fieldName) {
225+
return new AggregateFunction("first", fieldName);
226+
}
227+
228+
/**
229+
* Creates an aggregation that finds the first value of an expression across multiple stage
230+
* inputs.
231+
*
232+
* @param expression The expression to find the first value of.
233+
* @return A new {@link AggregateFunction} representing the first aggregation.
234+
*/
235+
@BetaApi
236+
public static AggregateFunction first(Expression expression) {
237+
return new AggregateFunction("first", expression);
238+
}
239+
240+
/**
241+
* Creates an aggregation that finds the last value of a field across multiple stage inputs.
242+
*
243+
* @param fieldName The name of the field to find the last value of.
244+
* @return A new {@link AggregateFunction} representing the last aggregation.
245+
*/
246+
@BetaApi
247+
public static AggregateFunction last(String fieldName) {
248+
return new AggregateFunction("last", fieldName);
249+
}
250+
251+
/**
252+
* Creates an aggregation that finds the last value of an expression across multiple stage inputs.
253+
*
254+
* @param expression The expression to find the last value of.
255+
* @return A new {@link AggregateFunction} representing the last aggregation.
256+
*/
257+
@BetaApi
258+
public static AggregateFunction last(Expression expression) {
259+
return new AggregateFunction("last", expression);
260+
}
261+
262+
/**
263+
* Creates an aggregation that collects all values of a field across multiple stage inputs into an
264+
* array.
265+
*
266+
* <p>If the expression resolves to an absent value, it is converted to `null`. The order of
267+
* elements in the output array is not stable and shouldn't be relied upon.
268+
*
269+
* @param fieldName The name of the field to collect values from.
270+
* @return A new {@link AggregateFunction} representing the array_agg aggregation.
271+
*/
272+
@BetaApi
273+
public static AggregateFunction arrayAgg(String fieldName) {
274+
return new AggregateFunction("array_agg", fieldName);
275+
}
276+
277+
/**
278+
* Creates an aggregation that collects all values of an expression across multiple stage inputs
279+
* into an array.
280+
*
281+
* <p>If the expression resolves to an absent value, it is converted to `null`. The order of
282+
* elements in the output array is not stable and shouldn't be relied upon.
283+
*
284+
* @param expression The expression to collect values from.
285+
* @return A new {@link AggregateFunction} representing the array_agg aggregation.
286+
*/
287+
@BetaApi
288+
public static AggregateFunction arrayAgg(Expression expression) {
289+
return new AggregateFunction("array_agg", expression);
290+
}
291+
292+
/**
293+
* Creates an aggregation that collects all distinct values of a field across multiple stage
294+
* inputs into an array.
295+
*
296+
* <p>If the expression resolves to an absent value, it is converted to `null`. The order of
297+
* elements in the output array is not stable and shouldn't be relied upon.
298+
*
299+
* @param fieldName The name of the field to collect values from.
300+
* @return A new {@link AggregateFunction} representing the array_agg_distinct aggregation.
301+
*/
302+
@BetaApi
303+
public static AggregateFunction arrayAggDistinct(String fieldName) {
304+
return new AggregateFunction("array_agg_distinct", fieldName);
305+
}
306+
307+
/**
308+
* Creates an aggregation that collects all distinct values of an expression across multiple stage
309+
* inputs into an array.
310+
*
311+
* <p>If the expression resolves to an absent value, it is converted to `null`. The order of
312+
* elements in the output array is not stable and shouldn't be relied upon.
313+
*
314+
* @param expression The expression to collect values from.
315+
* @return A new {@link AggregateFunction} representing the array_agg_distinct aggregation.
316+
*/
317+
@BetaApi
318+
public static AggregateFunction arrayAggDistinct(Expression expression) {
319+
return new AggregateFunction("array_agg_distinct", expression);
320+
}
321+
217322
/**
218323
* Assigns an alias to this aggregate.
219324
*

google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4593,6 +4593,56 @@ public final AggregateFunction countDistinct() {
45934593
return AggregateFunction.countDistinct(this);
45944594
}
45954595

4596+
/**
4597+
* Creates an aggregation that finds the first value of this expression across multiple stage
4598+
* inputs.
4599+
*
4600+
* @return A new {@link AggregateFunction} representing the first aggregation.
4601+
*/
4602+
@BetaApi
4603+
public final AggregateFunction first() {
4604+
return AggregateFunction.first(this);
4605+
}
4606+
4607+
/**
4608+
* Creates an aggregation that finds the last value of this expression across multiple stage
4609+
* inputs.
4610+
*
4611+
* @return A new {@link AggregateFunction} representing the last aggregation.
4612+
*/
4613+
@BetaApi
4614+
public final AggregateFunction last() {
4615+
return AggregateFunction.last(this);
4616+
}
4617+
4618+
/**
4619+
* Creates an aggregation that collects all values of this expression across multiple stage inputs
4620+
* into an array.
4621+
*
4622+
* <p>If the expression resolves to an absent value, it is converted to `null`. The order of
4623+
* elements in the output array is not stable and shouldn't be relied upon.
4624+
*
4625+
* @return A new {@link AggregateFunction} representing the array_agg aggregation.
4626+
*/
4627+
@BetaApi
4628+
public final AggregateFunction arrayAgg() {
4629+
return AggregateFunction.arrayAgg(this);
4630+
}
4631+
4632+
/**
4633+
* Creates an aggregation that collects all distinct values of this expression across multiple
4634+
* stage inputs into an array.
4635+
*
4636+
* <p>If the expression resolves to an absent value, it is converted to `null`. The order of
4637+
* elements in the output array is not stable and shouldn't be relied upon.
4638+
*
4639+
* @return A new {@link AggregateFunction} representing the array_agg_distinct aggregation.
4640+
*/
4641+
@BetaApi
4642+
public final AggregateFunction arrayAggDistinct() {
4643+
return AggregateFunction.arrayAggDistinct(this);
4644+
}
4645+
45964646
/**
45974647
* Create an {@link Ordering} that sorts documents in ascending order based on value of this
45984648
* expression

google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,14 @@
1919
import static com.google.cloud.firestore.FieldValue.vector;
2020
import static com.google.cloud.firestore.it.ITQueryTest.map;
2121
import static com.google.cloud.firestore.it.TestHelper.isRunningAgainstFirestoreEmulator;
22+
import static com.google.cloud.firestore.pipeline.expressions.AggregateFunction.arrayAgg;
23+
import static com.google.cloud.firestore.pipeline.expressions.AggregateFunction.arrayAggDistinct;
2224
import static com.google.cloud.firestore.pipeline.expressions.AggregateFunction.count;
2325
import static com.google.cloud.firestore.pipeline.expressions.AggregateFunction.countAll;
2426
import static com.google.cloud.firestore.pipeline.expressions.AggregateFunction.countDistinct;
2527
import static com.google.cloud.firestore.pipeline.expressions.AggregateFunction.countIf;
28+
import static com.google.cloud.firestore.pipeline.expressions.AggregateFunction.first;
29+
import static com.google.cloud.firestore.pipeline.expressions.AggregateFunction.last;
2630
import static com.google.cloud.firestore.pipeline.expressions.AggregateFunction.sum;
2731
import static com.google.cloud.firestore.pipeline.expressions.Expression.add;
2832
import static com.google.cloud.firestore.pipeline.expressions.Expression.and;
@@ -583,6 +587,132 @@ public void testMinMax() throws Exception {
583587
"min_published", 1813L)));
584588
}
585589

590+
@Test
591+
public void testFirstAndLastAccumulators() throws Exception {
592+
List<PipelineResult> results =
593+
firestore
594+
.pipeline()
595+
.createFrom(collection)
596+
.where(field("published").greaterThan(0))
597+
.sort(field("published").ascending())
598+
.aggregate(
599+
first("rating").as("firstBookRating"),
600+
first("title").as("firstBookTitle"),
601+
last("rating").as("lastBookRating"),
602+
last("title").as("lastBookTitle"))
603+
.execute()
604+
.get()
605+
.getResults();
606+
607+
Map<String, Object> result = data(results).get(0);
608+
assertThat(result.get("firstBookRating")).isEqualTo(4.5);
609+
assertThat(result.get("firstBookTitle")).isEqualTo("Pride and Prejudice");
610+
assertThat(result.get("lastBookRating")).isEqualTo(4.1);
611+
assertThat(result.get("lastBookTitle")).isEqualTo("The Handmaid's Tale");
612+
}
613+
614+
@Test
615+
public void testFirstAndLastAccumulatorsWithInstanceMethod() throws Exception {
616+
List<PipelineResult> results =
617+
firestore
618+
.pipeline()
619+
.createFrom(collection)
620+
.where(field("published").greaterThan(0))
621+
.sort(field("published").ascending())
622+
.aggregate(
623+
field("rating").first().as("firstBookRating"),
624+
field("title").first().as("firstBookTitle"),
625+
field("rating").last().as("lastBookRating"),
626+
field("title").last().as("lastBookTitle"))
627+
.execute()
628+
.get()
629+
.getResults();
630+
631+
Map<String, Object> result = data(results).get(0);
632+
assertThat(result.get("firstBookRating")).isEqualTo(4.5);
633+
assertThat(result.get("firstBookTitle")).isEqualTo("Pride and Prejudice");
634+
assertThat(result.get("lastBookRating")).isEqualTo(4.1);
635+
assertThat(result.get("lastBookTitle")).isEqualTo("The Handmaid's Tale");
636+
}
637+
638+
@Test
639+
public void testArrayAggAccumulators() throws Exception {
640+
List<PipelineResult> results =
641+
firestore
642+
.pipeline()
643+
.createFrom(collection)
644+
.where(field("published").greaterThan(0))
645+
.sort(field("published").ascending())
646+
.aggregate(arrayAgg("rating").as("allRatings"))
647+
.execute()
648+
.get()
649+
.getResults();
650+
651+
Map<String, Object> result = data(results).get(0);
652+
assertThat((List<?>) result.get("allRatings"))
653+
.containsExactly(4.5, 4.3, 4.0, 4.2, 4.7, 4.2, 4.6, 4.3, 4.2, 4.1)
654+
.inOrder();
655+
}
656+
657+
@Test
658+
public void testArrayAggAccumulatorsWithInstanceMethod() throws Exception {
659+
List<PipelineResult> results =
660+
firestore
661+
.pipeline()
662+
.createFrom(collection)
663+
.where(field("published").greaterThan(0))
664+
.sort(field("published").ascending())
665+
.aggregate(field("rating").arrayAgg().as("allRatings"))
666+
.execute()
667+
.get()
668+
.getResults();
669+
670+
Map<String, Object> result = data(results).get(0);
671+
assertThat((List<?>) result.get("allRatings"))
672+
.containsExactly(4.5, 4.3, 4.0, 4.2, 4.7, 4.2, 4.6, 4.3, 4.2, 4.1)
673+
.inOrder();
674+
}
675+
676+
@Test
677+
public void testArrayAggDistinctAccumulators() throws Exception {
678+
List<PipelineResult> results =
679+
firestore
680+
.pipeline()
681+
.createFrom(collection)
682+
.where(field("published").greaterThan(0))
683+
.aggregate(arrayAggDistinct("rating").as("allDistinctRatings"))
684+
.execute()
685+
.get()
686+
.getResults();
687+
688+
Map<String, Object> result = data(results).get(0);
689+
List<?> distinctRatings = (List<?>) result.get("allDistinctRatings");
690+
List<Double> sortedRatings =
691+
distinctRatings.stream().map(o -> (Double) o).sorted().collect(Collectors.toList());
692+
693+
assertThat(sortedRatings).containsExactly(4.0, 4.1, 4.2, 4.3, 4.5, 4.6, 4.7).inOrder();
694+
}
695+
696+
@Test
697+
public void testArrayAggDistinctAccumulatorsWithInstanceMethod() throws Exception {
698+
List<PipelineResult> results =
699+
firestore
700+
.pipeline()
701+
.createFrom(collection)
702+
.where(field("published").greaterThan(0))
703+
.aggregate(field("rating").arrayAggDistinct().as("allDistinctRatings"))
704+
.execute()
705+
.get()
706+
.getResults();
707+
708+
Map<String, Object> result = data(results).get(0);
709+
List<?> distinctRatings = (List<?>) result.get("allDistinctRatings");
710+
List<Double> sortedRatings =
711+
distinctRatings.stream().map(o -> (Double) o).sorted().collect(Collectors.toList());
712+
713+
assertThat(sortedRatings).containsExactly(4.0, 4.1, 4.2, 4.3, 4.5, 4.6, 4.7).inOrder();
714+
}
715+
586716
@Test
587717
public void selectSpecificFields() throws Exception {
588718
List<PipelineResult> results =

0 commit comments

Comments
 (0)