From 2a8213cf5ff647529960c84c528bbddd2677ccae Mon Sep 17 00:00:00 2001 From: Linchin Date: Thu, 5 Mar 2026 00:26:49 +0000 Subject: [PATCH 1/7] feat: literals pipeline stage --- .../cloud/firestore_v1/base_pipeline.py | 65 +++++++++++++++++++ .../cloud/firestore_v1/pipeline_stages.py | 17 +++++ .../tests/system/pipeline_e2e/general.yaml | 21 +++++- .../tests/unit/v1/test_pipeline.py | 1 + .../tests/unit/v1/test_pipeline_stages.py | 29 +++++++++ 5 files changed, 132 insertions(+), 1 deletion(-) 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..fe42785a9aad 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 @@ -275,6 +275,71 @@ def find_nearest( stages.FindNearest(field, vector, distance_measure, options) ) + def literals(self, *documents: str | Selectable) -> "_BasePipeline": + """ + Returns documents from a fixed set of predefined document objects. + + This stage is commonly used for testing other stages in isolation, + though it can also be used as inputs to join conditions. + + Example: + >>> from google.cloud.firestore_v1.pipeline_expressions import Constant + >>> documents = [ + ... {"name": "joe", "age": 10}, + ... {"name": "bob", "age": 30}, + ... {"name": "alice", "age": 40} + ... ] + >>> pipeline = client.pipeline() + ... .literals(Constant.of(documents)) + ... .where(field("age").lessThan(35)) + + Output documents: + ```json + [ + {"name": "joe", "age": 10}, + {"name": "bob", "age": 30} + ] + ``` + + Behavior: + The `literals(...)` stage can only be used as the first stage in a pipeline (or + sub-pipeline). The order of documents returned from the `literals` matches the + order in which they are defined. + + While literal values are the most common, it is also possible to pass in + expressions, which will be evaluated and returned, making it possible to test + out different query / expression behavior without first needing to create some + test data. + + For example, the following shows how to quickly test out the `length(...)` + function on some constant test sets: + + Example: + >>> from google.cloud.firestore_v1.pipeline_expressions import Constant + >>> documents = [ + ... {"x": Constant.of("foo-bar-baz").char_length()}, + ... {"x": Constant.of("bar").char_length()} + ... ] + >>> pipeline = client.pipeline().literals(Constant.of(documents)) + + Output documents: + ```json + [ + {"x": 11}, + {"x": 3} + ] + ``` + + Args: + documents: A `str` or `Selectable` expression. If a `str`, it's + treated as a field path to an array of documents. + If a `Selectable`, it's usually a `Constant` + containing an array of documents (as dictionaries). + Returns: + A new Pipeline object with this stage appended to the stage list. + """ + return self._append(stages.Literals(*documents)) + def replace_with( self, field: Selectable, 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 b00d923c673c..cd48960da1de 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 @@ -342,6 +342,23 @@ def _pb_args(self): return [Value(integer_value=self.limit)] +class Literals(Stage): + """Returns documents from a fixed set of predefined document objects.""" + + def __init__(self, *documents: str | Selectable): + super().__init__("literals") + self.documents = documents + + def _pb_args(self): + args = [] + for doc in self.documents: + if hasattr(doc, "_to_pb"): + args.append(doc._to_pb()) + else: + args.append(encode_value(doc)) + return args + + class Offset(Stage): """Skips a specified number of documents.""" diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml index 46a10cd4d1af..bcd81916be84 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml @@ -684,4 +684,23 @@ tests: - args: - fieldReferenceValue: awards - stringValue: full_replace - name: replace_with \ No newline at end of file + name: replace_with + - description: literals + pipeline: + - Literals: + - title: "The Hitchhiker's Guide to the Galaxy" + author: "Douglas Adams" + assert_results: + - title: "The Hitchhiker's Guide to the Galaxy" + author: "Douglas Adams" + assert_proto: + pipeline: + stages: + - args: + - mapValue: + fields: + author: + stringValue: "Douglas Adams" + title: + stringValue: "The Hitchhiker's Guide to the Galaxy" + name: literals \ No newline at end of file diff --git a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline.py b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline.py index 5953398709a3..d264f714ddaa 100644 --- a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline.py +++ b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline.py @@ -417,6 +417,7 @@ def test_pipeline_execute_stream_equivalence(): ("aggregate", (Field.of("n").as_("alias"),), stages.Aggregate), ("distinct", ("field_name",), stages.Distinct), ("distinct", (Field.of("n"), "second"), stages.Distinct), + ("literals", (Field.of("a"),), stages.Literals), ], ) def test_pipeline_methods(method, args, result_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 b32a6e5d3f13..e3693c8415d6 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 @@ -517,6 +517,35 @@ def test_to_pb(self): assert len(result.options) == 0 +class TestLiterals: + def _make_one(self, *args, **kwargs): + return stages.Literals(*args, **kwargs) + + def test_ctor(self): + val1 = Constant.of({"a": 1}) + val2 = Constant.of({"b": 2}) + instance = self._make_one(val1, val2) + assert instance.documents == (val1, val2) + assert instance.name == "literals" + + def test_repr(self): + val1 = Constant.of({"a": 1}) + instance = self._make_one(val1) + repr_str = repr(instance) + assert repr_str == "Literals(documents=(Constant.of({'a': 1}),))" + + def test_to_pb(self): + val1 = Constant.of({"a": 1}) + val2 = Constant.of({"b": 2}) + instance = self._make_one(val1, val2) + result = instance._to_pb() + assert result.name == "literals" + assert len(result.args) == 2 + assert result.args[0].map_value.fields["a"].integer_value == 1 + assert result.args[1].map_value.fields["b"].integer_value == 2 + assert len(result.options) == 0 + + class TestOffset: def _make_one(self, *args, **kwargs): return stages.Offset(*args, **kwargs) From 28acee915967602e06f179e164242c6aa5cdf7df Mon Sep 17 00:00:00 2001 From: Linchin Date: Fri, 6 Mar 2026 21:37:01 +0000 Subject: [PATCH 2/7] add dict as supported type and update docstring --- .../google/cloud/firestore_v1/base_pipeline.py | 2 +- .../google/cloud/firestore_v1/pipeline_stages.py | 2 +- .../tests/unit/v1/test_pipeline_stages.py | 8 ++++---- 3 files changed, 6 insertions(+), 6 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 fe42785a9aad..02e2ceecaa52 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 @@ -275,7 +275,7 @@ def find_nearest( stages.FindNearest(field, vector, distance_measure, options) ) - def literals(self, *documents: str | Selectable) -> "_BasePipeline": + def literals(self, *documents: Selectable | dict) -> "_BasePipeline": """ Returns documents from a fixed set of predefined document objects. 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 cd48960da1de..9ccb1d5aaa69 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 @@ -345,7 +345,7 @@ def _pb_args(self): class Literals(Stage): """Returns documents from a fixed set of predefined document objects.""" - def __init__(self, *documents: str | Selectable): + def __init__(self, *documents: Selectable | dict): super().__init__("literals") self.documents = documents 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 e3693c8415d6..82fe3e1881b2 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 @@ -523,20 +523,20 @@ def _make_one(self, *args, **kwargs): def test_ctor(self): val1 = Constant.of({"a": 1}) - val2 = Constant.of({"b": 2}) + val2 = {"b": 2} instance = self._make_one(val1, val2) assert instance.documents == (val1, val2) assert instance.name == "literals" def test_repr(self): val1 = Constant.of({"a": 1}) - instance = self._make_one(val1) + instance = self._make_one(val1, {"b": 2}) repr_str = repr(instance) - assert repr_str == "Literals(documents=(Constant.of({'a': 1}),))" + assert repr_str == "Literals(documents=(Constant.of({'a': 1}), {'b': 2}))" def test_to_pb(self): val1 = Constant.of({"a": 1}) - val2 = Constant.of({"b": 2}) + val2 = {"b": 2} instance = self._make_one(val1, val2) result = instance._to_pb() assert result.name == "literals" From a2e11ed359427538ee8a2027c846cbba7006c940 Mon Sep 17 00:00:00 2001 From: Linchin Date: Mon, 9 Mar 2026 19:13:40 +0000 Subject: [PATCH 3/7] correct docstring and type annotation --- .../google/cloud/firestore_v1/base_pipeline.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 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 02e2ceecaa52..4db636fe9d41 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 @@ -275,7 +275,7 @@ def find_nearest( stages.FindNearest(field, vector, distance_measure, options) ) - def literals(self, *documents: Selectable | dict) -> "_BasePipeline": + def literals(self, *documents: Expression | dict) -> "_BasePipeline": """ Returns documents from a fixed set of predefined document objects. @@ -290,7 +290,7 @@ def literals(self, *documents: Selectable | dict) -> "_BasePipeline": ... {"name": "alice", "age": 40} ... ] >>> pipeline = client.pipeline() - ... .literals(Constant.of(documents)) + ... .literals(documents) ... .where(field("age").lessThan(35)) Output documents: @@ -320,7 +320,7 @@ def literals(self, *documents: Selectable | dict) -> "_BasePipeline": ... {"x": Constant.of("foo-bar-baz").char_length()}, ... {"x": Constant.of("bar").char_length()} ... ] - >>> pipeline = client.pipeline().literals(Constant.of(documents)) + >>> pipeline = client.pipeline().literals(documents) Output documents: ```json @@ -331,10 +331,8 @@ def literals(self, *documents: Selectable | dict) -> "_BasePipeline": ``` Args: - documents: A `str` or `Selectable` expression. If a `str`, it's - treated as a field path to an array of documents. - If a `Selectable`, it's usually a `Constant` - containing an array of documents (as dictionaries). + documents: One or more documents to be returned by this stage. Each can be a `dict` + or an `Expression`. Returns: A new Pipeline object with this stage appended to the stage list. """ From a97cb134040b0e429b83bfd60d6ae79b5c9dd57c Mon Sep 17 00:00:00 2001 From: Linchin Date: Mon, 9 Mar 2026 19:14:55 +0000 Subject: [PATCH 4/7] type annotation --- .../google/cloud/firestore_v1/pipeline_stages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9ccb1d5aaa69..30d2fd8e936a 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 @@ -345,7 +345,7 @@ def _pb_args(self): class Literals(Stage): """Returns documents from a fixed set of predefined document objects.""" - def __init__(self, *documents: Selectable | dict): + def __init__(self, *documents: Expression | dict): super().__init__("literals") self.documents = documents From cc1b737571cdc660ed6cdee9e407814900d91b81 Mon Sep 17 00:00:00 2001 From: Linchin Date: Mon, 9 Mar 2026 20:33:45 +0000 Subject: [PATCH 5/7] add more unit tests --- packages/google-cloud-firestore/tests/unit/v1/test_pipeline.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline.py b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline.py index d264f714ddaa..5ec2e9ac1123 100644 --- a/packages/google-cloud-firestore/tests/unit/v1/test_pipeline.py +++ b/packages/google-cloud-firestore/tests/unit/v1/test_pipeline.py @@ -418,6 +418,8 @@ def test_pipeline_execute_stream_equivalence(): ("distinct", ("field_name",), stages.Distinct), ("distinct", (Field.of("n"), "second"), stages.Distinct), ("literals", (Field.of("a"),), stages.Literals), + ("literals", ({"name": "joe"}, {"name": "bob"}), stages.Literals), + ("literals", (Field.of("a"), {"name": "joe"}), stages.Literals), ], ) def test_pipeline_methods(method, args, result_cls): From 386122314ff9b244be18f81a7a4a482ec0540762 Mon Sep 17 00:00:00 2001 From: Linchin Date: Mon, 9 Mar 2026 20:35:46 +0000 Subject: [PATCH 6/7] correct docstring --- .../google/cloud/firestore_v1/base_pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4db636fe9d41..5b7f234f7d0f 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 @@ -331,7 +331,7 @@ def literals(self, *documents: Expression | dict) -> "_BasePipeline": ``` Args: - documents: One or more documents to be returned by this stage. Each can be a `dict` + *documents: One or more documents to be returned by this stage. Each can be a `dict` or an `Expression`. Returns: A new Pipeline object with this stage appended to the stage list. From 7bad13cbf5b8636a422565accc6054911a5cc6fc Mon Sep 17 00:00:00 2001 From: Linchin Date: Mon, 9 Mar 2026 21:11:38 +0000 Subject: [PATCH 7/7] add cases to system test --- .../tests/system/pipeline_e2e/general.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml b/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml index bcd81916be84..37fbbdd54dc6 100644 --- a/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml +++ b/packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml @@ -690,6 +690,10 @@ tests: - Literals: - title: "The Hitchhiker's Guide to the Galaxy" author: "Douglas Adams" + - Constant: + value: + genre: "Science Fiction" + year: 1979 assert_results: - title: "The Hitchhiker's Guide to the Galaxy" author: "Douglas Adams" @@ -703,4 +707,10 @@ tests: stringValue: "Douglas Adams" title: stringValue: "The Hitchhiker's Guide to the Galaxy" + - mapValue: + fields: + genre: + stringValue: "Science Fiction" + year: + integerValue: '1979' name: literals \ No newline at end of file