Skip to content

Commit 2be5989

Browse files
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 <[email protected]>
1 parent 491c211 commit 2be5989

File tree

3 files changed

+285
-0
lines changed

3 files changed

+285
-0
lines changed

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
@@ -4427,6 +4427,56 @@ public final AggregateFunction countDistinct() {
44274427
return AggregateFunction.countDistinct(this);
44284428
}
44294429

4430+
/**
4431+
* Creates an aggregation that finds the first value of this expression across multiple stage
4432+
* inputs.
4433+
*
4434+
* @return A new {@link AggregateFunction} representing the first aggregation.
4435+
*/
4436+
@BetaApi
4437+
public final AggregateFunction first() {
4438+
return AggregateFunction.first(this);
4439+
}
4440+
4441+
/**
4442+
* Creates an aggregation that finds the last value of this expression across multiple stage
4443+
* inputs.
4444+
*
4445+
* @return A new {@link AggregateFunction} representing the last aggregation.
4446+
*/
4447+
@BetaApi
4448+
public final AggregateFunction last() {
4449+
return AggregateFunction.last(this);
4450+
}
4451+
4452+
/**
4453+
* Creates an aggregation that collects all values of this expression across multiple stage inputs
4454+
* into an array.
4455+
*
4456+
* <p>If the expression resolves to an absent value, it is converted to `null`. The order of
4457+
* elements in the output array is not stable and shouldn't be relied upon.
4458+
*
4459+
* @return A new {@link AggregateFunction} representing the array_agg aggregation.
4460+
*/
4461+
@BetaApi
4462+
public final AggregateFunction arrayAgg() {
4463+
return AggregateFunction.arrayAgg(this);
4464+
}
4465+
4466+
/**
4467+
* Creates an aggregation that collects all distinct values of this expression across multiple
4468+
* stage inputs into an array.
4469+
*
4470+
* <p>If the expression resolves to an absent value, it is converted to `null`. The order of
4471+
* elements in the output array is not stable and shouldn't be relied upon.
4472+
*
4473+
* @return A new {@link AggregateFunction} representing the array_agg_distinct aggregation.
4474+
*/
4475+
@BetaApi
4476+
public final AggregateFunction arrayAggDistinct() {
4477+
return AggregateFunction.arrayAggDistinct(this);
4478+
}
4479+
44304480
/**
44314481
* Create an {@link Ordering} that sorts documents in ascending order based on value of this
44324482
* 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;
@@ -582,6 +586,132 @@ public void testMinMax() throws Exception {
582586
"min_published", 1813L)));
583587
}
584588

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

0 commit comments

Comments
 (0)