From 8d69f0953f0c487f0d97f9b3478582042df5c780 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 27 Mar 2026 16:53:17 -0700 Subject: [PATCH 01/11] feat(firestore): add DML support to pipelines --- .../cloud/firestore_v1/base_pipeline.py | 52 ++++++++++++++++++ .../cloud/firestore_v1/pipeline_stages.py | 27 ++++++++++ .../tests/system/pipeline_e2e/dml.yaml | 23 ++++++++ .../tests/system/test_pipeline_acceptance.py | 53 +++++++++++++++++++ .../tests/unit/v1/test_pipeline_stages.py | 37 +++++++++++++ 5 files changed, 192 insertions(+) create mode 100644 packages/google-cloud-firestore/tests/system/pipeline_e2e/dml.yaml diff --git a/packages/google-cloud-firestore/google/cloud/firestore_v1/base_pipeline.py b/packages/google-cloud-firestore/google/cloud/firestore_v1/base_pipeline.py index fac7f8bc4bce..32d82d394429 100644 --- a/packages/google-cloud-firestore/google/cloud/firestore_v1/base_pipeline.py +++ b/packages/google-cloud-firestore/google/cloud/firestore_v1/base_pipeline.py @@ -610,3 +610,55 @@ def distinct(self, *fields: str | Selectable) -> "_BasePipeline": A new Pipeline object with this stage appended to the stage list """ return self._append(stages.Distinct(*fields)) + + def delete(self) -> "_BasePipeline": + """ + Deletes the documents from the current pipeline stage. + + Example: + >>> from google.cloud.firestore_v1.pipeline_expressions import Field + >>> pipeline = client.pipeline().collection("logs") + >>> # Delete all documents in the "logs" collection where "status" is "archived" + >>> pipeline = pipeline.where(Field.of("status").equal("archived")).delete() + >>> pipeline.execute() + + Returns: + A new Pipeline object with this stage appended to the stage list + """ + return self._append(stages.Delete()) + + def update(self, *transformed_fields: "Selectable") -> "_BasePipeline": + """ + Updates the documents with the given transformations. + + This method updates the documents in place based on the data flowing through the pipeline. + To specify transformations, use `*transformed_fields`. + + Example 1: Update a collection's schema by adding a new field and removing an old one. + >>> from google.cloud.firestore_v1.pipeline_expressions import Constant + >>> pipeline = client.pipeline().collection("books") + >>> pipeline = pipeline.add_fields(Constant.of("Fiction").as_("genre")) + >>> pipeline = pipeline.remove_fields("old_genre").update() + >>> pipeline.execute() + + Example 2: Update documents in place with data from literals. + >>> pipeline = client.pipeline().literals( + ... {"__name__": client.collection("books").document("book1"), "status": "Updated"} + ... ).update() + >>> pipeline.execute() + + Example 3: Update documents from previous stages with specified transformations. + >>> from google.cloud.firestore_v1.pipeline_expressions import Field, Constant + >>> pipeline = client.pipeline().collection("books") + >>> # Update the "status" field to "Discounted" for all books where price > 50 + >>> pipeline = pipeline.where(Field.of("price").greater_than(50)) + >>> pipeline = pipeline.update(Constant.of("Discounted").as_("status")) + >>> pipeline.execute() + + Args: + *transformed_fields: The transformations to apply. + + Returns: + A new Pipeline object with this stage appended to the stage list + """ + return self._append(stages.Update(*transformed_fields)) diff --git a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_stages.py b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_stages.py index cac9c70d4b99..af8a7a120379 100644 --- a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_stages.py +++ b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_stages.py @@ -494,3 +494,30 @@ def __init__(self, condition: BooleanExpression): def _pb_args(self): return [self.condition._to_pb()] + + +class Delete(Stage): + """Deletes documents matching the pipeline criteria.""" + + def __init__(self): + super().__init__("delete") + + def _pb_args(self) -> list[Value]: + return [] + + def _pb_options(self) -> dict[str, Value]: + return {} + + +class Update(Stage): + """Updates documents with transformed fields.""" + + def __init__(self, *transformed_fields: Selectable): + super().__init__("update") + self.transformed_fields = list(transformed_fields) + + def _pb_args(self) -> list[Value]: + return [Selectable._to_value(self.transformed_fields)] + + def _pb_options(self) -> dict[str, Value]: + return {} diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/dml.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/dml.yaml new file mode 100644 index 000000000000..46c7a4c48861 --- /dev/null +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/dml.yaml @@ -0,0 +1,23 @@ +data: + dml_delete_coll: + doc1: { score: 10 } + doc2: { score: 60 } + dml_update_coll: + doc1: { status: "pending", score: 50 } + +tests: + - description: "Basic DML delete" + pipeline: + - Collection: dml_delete_coll + - Where: Field.of("score").less_than(50) + - Delete: + assert_end_state: + dml_delete_coll/doc1: null + dml_delete_coll/doc2: { score: 60 } + + - description: "Basic DML update" + pipeline: + - Collection: dml_update_coll + - Update: Field.of("status").set("active") + assert_end_state: + dml_update_coll/doc1: { status: "active", score: 50 } diff --git a/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py b/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py index afff43ac6950..5ec600b3fd48 100644 --- a/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py +++ b/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py @@ -206,6 +206,59 @@ async def test_pipeline_results_async(test_dict, async_client): assert len(got_results) == expected_count +@pytest.mark.parametrize( + "test_dict", + [t for t in yaml_loader() if "assert_end_state" in t], + ids=id_format, +) +def test_pipeline_end_state(test_dict, client): + """ + Ensure pipeline leaves database in expected state + """ + expected_state = _parse_yaml_types(test_dict["assert_end_state"]) + pipeline = parse_pipeline(client, test_dict["pipeline"]) + + # Execute the DML pipeline + pipeline.execute() + + # Assert end state + for doc_path, expected_content in expected_state.items(): + doc_ref = client.document(doc_path) + snapshot = doc_ref.get() + if expected_content is None or expected_content == {}: + assert not snapshot.exists, f"Expected {doc_path} to be deleted, but it exists" + else: + assert snapshot.exists, f"Expected {doc_path} to exist, but it was deleted" + assert snapshot.to_dict() == expected_content + + +@pytest.mark.parametrize( + "test_dict", + [t for t in yaml_loader() if "assert_end_state" in t], + ids=id_format, +) +@pytest.mark.asyncio +async def test_pipeline_end_state_async(test_dict, async_client): + """ + Ensure pipeline leaves database in expected state + """ + expected_state = _parse_yaml_types(test_dict["assert_end_state"]) + pipeline = parse_pipeline(async_client, test_dict["pipeline"]) + + # Execute the DML pipeline + await pipeline.execute() + + # Assert end state + for doc_path, expected_content in expected_state.items(): + doc_ref = async_client.document(doc_path) + snapshot = await doc_ref.get() + if expected_content is None or expected_content == {}: + assert not snapshot.exists, f"Expected {doc_path} to be deleted, but it exists" + else: + assert snapshot.exists, f"Expected {doc_path} to exist, but it was deleted" + assert snapshot.to_dict() == expected_content + + ################################################################################# # Helpers & Fixtures ################################################################################# diff --git a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_stages.py b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_stages.py index 65685e6e33d6..60bf83c96de3 100644 --- a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_stages.py +++ b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_stages.py @@ -960,3 +960,40 @@ def test_to_pb(self): assert got_fn.args[0].field_reference_value == "city" assert got_fn.args[1].string_value == "SF" assert len(result.options) == 0 + + +class TestDelete: + def _make_one(self): + return stages.Delete() + + def test_to_pb(self): + instance = self._make_one() + result = instance._to_pb() + assert result.name == "delete" + assert len(result.args) == 0 + assert len(result.options) == 0 + + +class TestUpdate: + def _make_one(self, *args): + return stages.Update(*args) + + def test_to_pb_empty(self): + instance = self._make_one() + result = instance._to_pb() + assert result.name == "update" + assert len(result.args) == 1 + assert result.args[0].map_value.fields == {} + assert len(result.options) == 0 + + def test_to_pb_with_fields(self): + instance = self._make_one( + Field.of("score").add(10).as_("score"), + Field.of("status").set("active").as_("status") + ) + result = instance._to_pb() + assert result.name == "update" + assert len(result.args) == 1 + assert "score" in result.args[0].map_value.fields + assert "status" in result.args[0].map_value.fields + assert len(result.options) == 0 From 1a981b9d310f669d84ec9ad507d0436018e660f1 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Tue, 31 Mar 2026 14:59:43 -0700 Subject: [PATCH 02/11] updated yaml --- .../tests/system/pipeline_e2e/dml.yaml | 10 ++++++++-- .../tests/system/test_pipeline_acceptance.py | 5 ++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/dml.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/dml.yaml index 46c7a4c48861..8564f8112384 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/dml.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/dml.yaml @@ -9,7 +9,10 @@ tests: - description: "Basic DML delete" pipeline: - Collection: dml_delete_coll - - Where: Field.of("score").less_than(50) + - Where: + FunctionExpression.less_than: + - Field: score + - Constant: 50 - Delete: assert_end_state: dml_delete_coll/doc1: null @@ -18,6 +21,9 @@ tests: - description: "Basic DML update" pipeline: - Collection: dml_update_coll - - Update: Field.of("status").set("active") + - Update: + - AliasedExpression: + - Constant: "active" + - "status" assert_end_state: dml_update_coll/doc1: { status: "active", score: 50 } diff --git a/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py b/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py index 5ec600b3fd48..e863b48a941d 100644 --- a/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py +++ b/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py @@ -276,7 +276,10 @@ def parse_pipeline(client, pipeline: list[dict[str, Any], str]): # find arguments if given if isinstance(stage, dict): stage_yaml_args = stage[stage_name] - stage_obj = _apply_yaml_args_to_callable(stage_cls, client, stage_yaml_args) + if stage_yaml_args is None: + stage_obj = stage_cls() + else: + stage_obj = _apply_yaml_args_to_callable(stage_cls, client, stage_yaml_args) else: # yaml has no arguments stage_obj = stage_cls() From ee61fa50dae87e43859d7fceee53f68332c4978c Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Tue, 31 Mar 2026 15:47:27 -0700 Subject: [PATCH 03/11] fixed unit test --- .../tests/unit/v1/test_pipeline_stages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_stages.py b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_stages.py index 60bf83c96de3..f825a0e89d70 100644 --- a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_stages.py +++ b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_stages.py @@ -989,7 +989,7 @@ def test_to_pb_empty(self): def test_to_pb_with_fields(self): instance = self._make_one( Field.of("score").add(10).as_("score"), - Field.of("status").set("active").as_("status") + Constant.of("active").as_("status") ) result = instance._to_pb() assert result.name == "update" From cd2001b0a6e169696a00d842b30f5e1593e555b5 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Tue, 31 Mar 2026 15:55:13 -0700 Subject: [PATCH 04/11] added assert_proto sections --- .../tests/system/pipeline_e2e/dml.yaml | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/dml.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/dml.yaml index 8564f8112384..e03fe91868b3 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/dml.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/dml.yaml @@ -17,6 +17,21 @@ tests: assert_end_state: dml_delete_coll/doc1: null dml_delete_coll/doc2: { score: 60 } + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /dml_delete_coll + name: collection + - args: + - functionValue: + args: + - fieldReferenceValue: score + - integerValue: '50' + name: less_than + name: where + - args: [] + name: delete - description: "Basic DML update" pipeline: @@ -27,3 +42,15 @@ tests: - "status" assert_end_state: dml_update_coll/doc1: { status: "active", score: 50 } + assert_proto: + pipeline: + stages: + - args: + - referenceValue: /dml_update_coll + name: collection + - args: + - mapValue: + fields: + status: + stringValue: active + name: update From e997de819a3400143b5a4c1e2e52524a156a73b3 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 1 Apr 2026 16:27:41 -0700 Subject: [PATCH 05/11] fixed test parser --- .../tests/system/test_pipeline_acceptance.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py b/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py index e863b48a941d..cd1adf84e76f 100644 --- a/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py +++ b/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py @@ -335,18 +335,19 @@ def _apply_yaml_args_to_callable(callable_obj, client, yaml_args): Helper to instantiate a class with yaml arguments. The arguments will be applied as positional or keyword arguments, based on type """ - if isinstance(yaml_args, dict): - return callable_obj(**_parse_expressions(client, yaml_args)) + parsed = _parse_expressions(client, yaml_args) + if isinstance(yaml_args, dict) and isinstance(parsed, dict): + return callable_obj(**parsed) elif isinstance(yaml_args, list) and not ( callable_obj == expr.Constant or callable_obj == Vector or callable_obj == expr.Array ): # yaml has an array of arguments. Treat as args - return callable_obj(*_parse_expressions(client, yaml_args)) + return callable_obj(*parsed) else: # yaml has a single argument - return callable_obj(_parse_expressions(client, yaml_args)) + return callable_obj(parsed) def _is_expr_string(yaml_str): From 38755fbcf56014a148aa59bac46a3a7d0766c77d Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 1 Apr 2026 16:30:27 -0700 Subject: [PATCH 06/11] fixed assert_proto --- .../google-cloud-firestore/tests/system/pipeline_e2e/dml.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/dml.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/dml.yaml index e03fe91868b3..578ddef20492 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/dml.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/dml.yaml @@ -30,8 +30,7 @@ tests: - integerValue: '50' name: less_than name: where - - args: [] - name: delete + - name: delete - description: "Basic DML update" pipeline: From 814bc5bf51d7f4f930f051bedcd8446afd74f32b Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 1 Apr 2026 16:41:55 -0700 Subject: [PATCH 07/11] updated docstring --- .../google/cloud/firestore_v1/base_pipeline.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/google-cloud-firestore/google/cloud/firestore_v1/base_pipeline.py b/packages/google-cloud-firestore/google/cloud/firestore_v1/base_pipeline.py index 32d82d394429..1e1b7477bd92 100644 --- a/packages/google-cloud-firestore/google/cloud/firestore_v1/base_pipeline.py +++ b/packages/google-cloud-firestore/google/cloud/firestore_v1/base_pipeline.py @@ -629,10 +629,13 @@ def delete(self) -> "_BasePipeline": def update(self, *transformed_fields: "Selectable") -> "_BasePipeline": """ - Updates the documents with the given transformations. + Performs an update operation using documents from previous stages. - This method updates the documents in place based on the data flowing through the pipeline. - To specify transformations, use `*transformed_fields`. + If called without `transformed_fields`, this method updates the documents in + place based on the data flowing through the pipeline. + + To update specific fields with new values, provide `Selectable` expressions that define the + transformations to apply. Example 1: Update a collection's schema by adding a new field and removing an old one. >>> from google.cloud.firestore_v1.pipeline_expressions import Constant @@ -656,7 +659,8 @@ def update(self, *transformed_fields: "Selectable") -> "_BasePipeline": >>> pipeline.execute() Args: - *transformed_fields: The transformations to apply. + *transformed_fields: Optional. The transformations to apply. If not provided, + the update is performed in place based on the data flowing through the pipeline. Returns: A new Pipeline object with this stage appended to the stage list From 560bd0b4b20f6fad785f0a1059fc349167773028 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 1 Apr 2026 16:42:14 -0700 Subject: [PATCH 08/11] Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../tests/system/test_pipeline_acceptance.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py b/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py index cd1adf84e76f..cd49bd9401a4 100644 --- a/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py +++ b/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py @@ -225,7 +225,7 @@ def test_pipeline_end_state(test_dict, client): for doc_path, expected_content in expected_state.items(): doc_ref = client.document(doc_path) snapshot = doc_ref.get() - if expected_content is None or expected_content == {}: + if expected_content is None: assert not snapshot.exists, f"Expected {doc_path} to be deleted, but it exists" else: assert snapshot.exists, f"Expected {doc_path} to exist, but it was deleted" @@ -252,7 +252,7 @@ async def test_pipeline_end_state_async(test_dict, async_client): for doc_path, expected_content in expected_state.items(): doc_ref = async_client.document(doc_path) snapshot = await doc_ref.get() - if expected_content is None or expected_content == {}: + if expected_content is None: assert not snapshot.exists, f"Expected {doc_path} to be deleted, but it exists" else: assert snapshot.exists, f"Expected {doc_path} to exist, but it was deleted" From 8e7fab3f0fca8ca8896f661f5dc892c74ef8dbc2 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 1 Apr 2026 16:44:42 -0700 Subject: [PATCH 09/11] fixed lint --- .../cloud/firestore_v1/pipeline_stages.py | 6 -- .../tests/system/test_pipeline_acceptance.py | 57 +------------------ .../tests/unit/v1/test_pipeline_stages.py | 3 +- 3 files changed, 4 insertions(+), 62 deletions(-) diff --git a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_stages.py b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_stages.py index af8a7a120379..9de782f3cbfa 100644 --- a/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_stages.py +++ b/packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_stages.py @@ -505,9 +505,6 @@ def __init__(self): def _pb_args(self) -> list[Value]: return [] - def _pb_options(self) -> dict[str, Value]: - return {} - class Update(Stage): """Updates documents with transformed fields.""" @@ -518,6 +515,3 @@ def __init__(self, *transformed_fields: Selectable): def _pb_args(self) -> list[Value]: return [Selectable._to_value(self.transformed_fields)] - - def _pb_options(self) -> dict[str, Value]: - return {} diff --git a/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py b/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py index cd49bd9401a4..644b06e10ffb 100644 --- a/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py +++ b/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py @@ -206,59 +206,6 @@ async def test_pipeline_results_async(test_dict, async_client): assert len(got_results) == expected_count -@pytest.mark.parametrize( - "test_dict", - [t for t in yaml_loader() if "assert_end_state" in t], - ids=id_format, -) -def test_pipeline_end_state(test_dict, client): - """ - Ensure pipeline leaves database in expected state - """ - expected_state = _parse_yaml_types(test_dict["assert_end_state"]) - pipeline = parse_pipeline(client, test_dict["pipeline"]) - - # Execute the DML pipeline - pipeline.execute() - - # Assert end state - for doc_path, expected_content in expected_state.items(): - doc_ref = client.document(doc_path) - snapshot = doc_ref.get() - if expected_content is None: - assert not snapshot.exists, f"Expected {doc_path} to be deleted, but it exists" - else: - assert snapshot.exists, f"Expected {doc_path} to exist, but it was deleted" - assert snapshot.to_dict() == expected_content - - -@pytest.mark.parametrize( - "test_dict", - [t for t in yaml_loader() if "assert_end_state" in t], - ids=id_format, -) -@pytest.mark.asyncio -async def test_pipeline_end_state_async(test_dict, async_client): - """ - Ensure pipeline leaves database in expected state - """ - expected_state = _parse_yaml_types(test_dict["assert_end_state"]) - pipeline = parse_pipeline(async_client, test_dict["pipeline"]) - - # Execute the DML pipeline - await pipeline.execute() - - # Assert end state - for doc_path, expected_content in expected_state.items(): - doc_ref = async_client.document(doc_path) - snapshot = await doc_ref.get() - if expected_content is None: - assert not snapshot.exists, f"Expected {doc_path} to be deleted, but it exists" - else: - assert snapshot.exists, f"Expected {doc_path} to exist, but it was deleted" - assert snapshot.to_dict() == expected_content - - ################################################################################# # Helpers & Fixtures ################################################################################# @@ -279,7 +226,9 @@ def parse_pipeline(client, pipeline: list[dict[str, Any], str]): if stage_yaml_args is None: stage_obj = stage_cls() else: - stage_obj = _apply_yaml_args_to_callable(stage_cls, client, stage_yaml_args) + stage_obj = _apply_yaml_args_to_callable( + stage_cls, client, stage_yaml_args + ) else: # yaml has no arguments stage_obj = stage_cls() diff --git a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_stages.py b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_stages.py index f825a0e89d70..b9ab603b713b 100644 --- a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_stages.py +++ b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline_stages.py @@ -988,8 +988,7 @@ def test_to_pb_empty(self): def test_to_pb_with_fields(self): instance = self._make_one( - Field.of("score").add(10).as_("score"), - Constant.of("active").as_("status") + Field.of("score").add(10).as_("score"), Constant.of("active").as_("status") ) result = instance._to_pb() assert result.name == "update" From 8d86c8aa9845ed65e61fdfe8ad2007f929bb1a86 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 1 Apr 2026 16:56:18 -0700 Subject: [PATCH 10/11] combined end state tests into standard pipeline methods --- .../tests/system/test_pipeline_acceptance.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py b/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py index 644b06e10ffb..8583d06a0f07 100644 --- a/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py +++ b/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py @@ -119,6 +119,7 @@ def test_pipeline_expected_errors(test_dict, client): if "assert_results" in t or "assert_count" in t or "assert_results_approximate" in t + or "assert_end_state" in t ], ids=id_format, ) @@ -131,6 +132,7 @@ def test_pipeline_results(test_dict, client): test_dict.get("assert_results_approximate", None) ) expected_count = test_dict.get("assert_count", None) + expected_end_state = _parse_yaml_types(test_dict.get("assert_end_state", {})) pipeline = parse_pipeline(client, test_dict["pipeline"]) # check if server responds as expected got_results = [snapshot.data() for snapshot in pipeline.stream()] @@ -146,6 +148,17 @@ def test_pipeline_results(test_dict, client): ) if expected_count is not None: assert len(got_results) == expected_count + if expected_end_state: + for doc_path, expected_content in expected_end_state.items(): + doc_ref = client.document(doc_path) + snapshot = doc_ref.get() + if expected_content is None: + assert not snapshot.exists, ( + f"Expected {doc_path} to be absent, but it exists" + ) + else: + assert snapshot.exists, f"Expected {doc_path} to exist, but it was absent" + assert snapshot.to_dict() == expected_content @pytest.mark.parametrize( @@ -176,6 +189,7 @@ async def test_pipeline_expected_errors_async(test_dict, async_client): if "assert_results" in t or "assert_count" in t or "assert_results_approximate" in t + or "assert_end_state" in t ], ids=id_format, ) @@ -189,6 +203,7 @@ async def test_pipeline_results_async(test_dict, async_client): test_dict.get("assert_results_approximate", None) ) expected_count = test_dict.get("assert_count", None) + expected_end_state = _parse_yaml_types(test_dict.get("assert_end_state", {})) pipeline = parse_pipeline(async_client, test_dict["pipeline"]) # check if server responds as expected got_results = [snapshot.data() async for snapshot in pipeline.stream()] @@ -204,6 +219,17 @@ async def test_pipeline_results_async(test_dict, async_client): ) if expected_count is not None: assert len(got_results) == expected_count + if expected_end_state: + for doc_path, expected_content in expected_end_state.items(): + doc_ref = async_client.document(doc_path) + snapshot = await doc_ref.get() + if expected_content is None: + assert not snapshot.exists, ( + f"Expected {doc_path} to be absent, but it exists" + ) + else: + assert snapshot.exists, f"Expected {doc_path} to exist, but it was absent" + assert snapshot.to_dict() == expected_content ################################################################################# From 692bfdcc88f069af7e4d579064c71d7402404df8 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 1 Apr 2026 17:02:55 -0700 Subject: [PATCH 11/11] fixed lint --- .../tests/system/test_pipeline_acceptance.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py b/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py index 8583d06a0f07..289ad165f7db 100644 --- a/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py +++ b/packages/google-cloud-firestore/tests/system/test_pipeline_acceptance.py @@ -157,7 +157,9 @@ def test_pipeline_results(test_dict, client): f"Expected {doc_path} to be absent, but it exists" ) else: - assert snapshot.exists, f"Expected {doc_path} to exist, but it was absent" + assert snapshot.exists, ( + f"Expected {doc_path} to exist, but it was absent" + ) assert snapshot.to_dict() == expected_content @@ -228,7 +230,9 @@ async def test_pipeline_results_async(test_dict, async_client): f"Expected {doc_path} to be absent, but it exists" ) else: - assert snapshot.exists, f"Expected {doc_path} to exist, but it was absent" + assert snapshot.exists, ( + f"Expected {doc_path} to exist, but it was absent" + ) assert snapshot.to_dict() == expected_content