From 48e833b2953465eaeaee7313e499ba2add35878a Mon Sep 17 00:00:00 2001 From: Linchin Date: Thu, 5 Mar 2026 00:46:40 +0000 Subject: [PATCH 1/3] feat(firestore): support array_agg(), array_agg_distinct(), first() and last() --- .../firestore_v1/pipeline_expressions.py | 44 +++++ .../tests/system/pipeline_e2e/aggregates.yaml | 163 +++++++++++++++++- .../unit/v1/test_pipeline_expressions.py | 36 ++++ 3 files changed, 242 insertions(+), 1 deletion(-) diff --git a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py index 376d785901fc..697c3dbbdbcb 100644 --- a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py +++ b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py @@ -1410,6 +1410,50 @@ def timestamp_to_unix_micros(self) -> "Expression": """ return FunctionExpression("timestamp_to_unix_micros", [self]) + @expose_as_static + def array_agg(self) -> "Expression": + """Creates an aggregation that collects values into an array. + Example: + >>> # Collect all values of 'color' into an array + >>> Field.of("color").array_agg() + Returns: + A new `AggregateFunction` representing the array aggregation. + """ + return AggregateFunction("array_agg", [self]) + + @expose_as_static + def array_agg_distinct(self) -> "Expression": + """Creates an aggregation that collects distinct values into an array. + Example: + >>> # Collect distinct values of 'color' into an array + >>> Field.of("color").array_agg_distinct() + Returns: + A new `AggregateFunction` representing the distinct array aggregation. + """ + return AggregateFunction("array_agg_distinct", [self]) + + @expose_as_static + def first(self) -> "Expression": + """Creates an aggregation that selecting the first value. + Example: + >>> # Select the first value of 'color' + >>> Field.of("color").first() + Returns: + A new `AggregateFunction` representing the first aggregation. + """ + return AggregateFunction("first", [self]) + + @expose_as_static + def last(self) -> "Expression": + """Creates an aggregation that selecting the last value. + Example: + >>> # Select the last value of 'color' + >>> Field.of("color").last() + Returns: + A new `AggregateFunction` representing the last aggregation. + """ + return AggregateFunction("last", [self]) + @expose_as_static def unix_micros_to_timestamp(self) -> "Expression": """Creates an expression that converts a number of microseconds since the epoch (1970-01-01 diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/aggregates.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/aggregates.yaml index 64a42698b758..2cd904b04c4b 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/aggregates.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/aggregates.yaml @@ -281,4 +281,165 @@ tests: - "total_rating" assert_results: - total_rating: 8.8 - + - description: testArrayAgg + pipeline: + - Collection: books + - Sort: + - Ordering: + - Field: title + - ASCENDING + - Aggregate: + - AliasedExpression: + - FunctionExpression.array_agg: + - Field: title + - "all_titles" + assert_results: + - all_titles: + - "1984" + - "Crime and Punishment" + - "Dune" + - "One Hundred Years of Solitude" + - "Pride and Prejudice" + - "The Great Gatsby" + - "The Handmaid's Tale" + - "The Hitchhiker's Guide to the Galaxy" + - "The Lord of the Rings" + - "To Kill a Mockingbird" + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - mapValue: + fields: + direction: + stringValue: ascending + expression: + fieldReferenceValue: title + name: sort + - args: + - mapValue: + fields: + all_titles: + functionValue: + name: array_agg + args: + - fieldReferenceValue: title + - mapValue: {} + name: aggregate + - description: testArrayAggDistinct + pipeline: + - Collection: books + - Where: + - FunctionExpression.equal: + - Field: genre + - Constant: Science Fiction + - Aggregate: + - AliasedExpression: + - FunctionExpression.array_agg_distinct: + - Field: genre + - "distinct_genres" + assert_results: + - distinct_genres: + - "Science Fiction" + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - functionValue: + args: + - fieldReferenceValue: genre + - stringValue: Science Fiction + name: equal + name: where + - args: + - mapValue: + fields: + distinct_genres: + functionValue: + name: array_agg_distinct + args: + - fieldReferenceValue: genre + - mapValue: {} + name: aggregate + - description: testFirst + pipeline: + - Collection: books + - Sort: + - Ordering: + - Field: title + - ASCENDING + - Aggregate: + - AliasedExpression: + - FunctionExpression.first: + - Field: title + - "first_title" + assert_results: + - first_title: "1984" + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - mapValue: + fields: + direction: + stringValue: ascending + expression: + fieldReferenceValue: title + name: sort + - args: + - mapValue: + fields: + first_title: + functionValue: + name: first + args: + - fieldReferenceValue: title + - mapValue: {} + name: aggregate + - description: testLast + pipeline: + - Collection: books + - Sort: + - Ordering: + - Field: title + - ASCENDING + - Aggregate: + - AliasedExpression: + - FunctionExpression.last: + - Field: title + - "last_title" + assert_results: + - last_title: "To Kill a Mockingbird" + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /books + name: collection + - args: + - mapValue: + fields: + direction: + stringValue: ascending + expression: + fieldReferenceValue: title + name: sort + - args: + - mapValue: + fields: + last_title: + functionValue: + name: last + args: + - fieldReferenceValue: title + - mapValue: {} + name: aggregate diff --git a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py index fe7beb460502..80738799f975 100644 --- a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py +++ b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_expressions.py @@ -1583,3 +1583,39 @@ def test_maximum(self): assert repr(instance) == "Value.maximum()" infix_instance = arg1.maximum() assert infix_instance == instance + + def test_array_agg(self): + arg1 = self._make_arg("Value") + instance = Expression.array_agg(arg1) + assert instance.name == "array_agg" + assert instance.params == [arg1] + assert repr(instance) == "Value.array_agg()" + infix_instance = arg1.array_agg() + assert infix_instance == instance + + def test_array_agg_distinct(self): + arg1 = self._make_arg("Value") + instance = Expression.array_agg_distinct(arg1) + assert instance.name == "array_agg_distinct" + assert instance.params == [arg1] + assert repr(instance) == "Value.array_agg_distinct()" + infix_instance = arg1.array_agg_distinct() + assert infix_instance == instance + + def test_first(self): + arg1 = self._make_arg("Value") + instance = Expression.first(arg1) + assert instance.name == "first" + assert instance.params == [arg1] + assert repr(instance) == "Value.first()" + infix_instance = arg1.first() + assert infix_instance == instance + + def test_last(self): + arg1 = self._make_arg("Value") + instance = Expression.last(arg1) + assert instance.name == "last" + assert instance.params == [arg1] + assert repr(instance) == "Value.last()" + infix_instance = arg1.last() + assert infix_instance == instance From e91f7a0e37730ec09281648545b5401263a86632 Mon Sep 17 00:00:00 2001 From: Lingqing Gan Date: Wed, 4 Mar 2026 17:01:23 -0800 Subject: [PATCH 2/3] Update packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../google/cloud/firestore_v1/pipeline_expressions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py index 697c3dbbdbcb..23adbba1647e 100644 --- a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py +++ b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py @@ -1434,7 +1434,7 @@ def array_agg_distinct(self) -> "Expression": @expose_as_static def first(self) -> "Expression": - """Creates an aggregation that selecting the first value. + """Creates an aggregation that selects the first value. Example: >>> # Select the first value of 'color' >>> Field.of("color").first() From 64e54df3e34776e02d0f970b55ccd196a45a3efa Mon Sep 17 00:00:00 2001 From: Lingqing Gan Date: Wed, 4 Mar 2026 17:01:29 -0800 Subject: [PATCH 3/3] Update packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../google/cloud/firestore_v1/pipeline_expressions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py index 23adbba1647e..2885dfa8d3a1 100644 --- a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py +++ b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py @@ -1445,7 +1445,7 @@ def first(self) -> "Expression": @expose_as_static def last(self) -> "Expression": - """Creates an aggregation that selecting the last value. + """Creates an aggregation that selects the last value. Example: >>> # Select the last value of 'color' >>> Field.of("color").last()