From d5f0db068b4f12800387ef1ee4d6bd3036eaccb1 Mon Sep 17 00:00:00 2001 From: Linchin Date: Fri, 13 Feb 2026 22:37:50 +0000 Subject: [PATCH 1/4] feat: literals pipeline stage --- google/cloud/firestore_v1/base_pipeline.py | 9 +++++++ google/cloud/firestore_v1/pipeline_stages.py | 11 +++++++++ tests/system/pipeline_e2e/general.yaml | 25 ++++++++++++++++++- tests/unit/v1/test_pipeline.py | 1 + tests/unit/v1/test_pipeline_stages.py | 26 ++++++++++++++++++++ 5 files changed, 71 insertions(+), 1 deletion(-) diff --git a/google/cloud/firestore_v1/base_pipeline.py b/google/cloud/firestore_v1/base_pipeline.py index 153564663..6dfcb39af 100644 --- a/google/cloud/firestore_v1/base_pipeline.py +++ b/google/cloud/firestore_v1/base_pipeline.py @@ -273,6 +273,15 @@ def find_nearest( stages.FindNearest(field, vector, distance_measure, options) ) + def literals( + self, + documents: Selectable, + ) -> "_BasePipeline": + """ + TODO: add docstring. + """ + return self._append(stages.Literals(documents)) + def replace_with( self, field: Selectable, diff --git a/google/cloud/firestore_v1/pipeline_stages.py b/google/cloud/firestore_v1/pipeline_stages.py index 18aa27044..c0845071b 100644 --- a/google/cloud/firestore_v1/pipeline_stages.py +++ b/google/cloud/firestore_v1/pipeline_stages.py @@ -342,6 +342,17 @@ def _pb_args(self): return [Value(integer_value=self.limit)] +class Literals(Stage): + """TODO: add docstring.""" + + def __init__(self, documents: Selectable): + super().__init__("literals") + self.documents = Field(documents) if isinstance(documents, str) else documents + + def _pb_args(self): + return [self.documents._to_pb()] + + class Offset(Stage): """Skips a specified number of documents.""" diff --git a/tests/system/pipeline_e2e/general.yaml b/tests/system/pipeline_e2e/general.yaml index 46a10cd4d..f0e0d87d6 100644 --- a/tests/system/pipeline_e2e/general.yaml +++ b/tests/system/pipeline_e2e/general.yaml @@ -684,4 +684,27 @@ tests: - args: - fieldReferenceValue: awards - stringValue: full_replace - name: replace_with \ No newline at end of file + name: replace_with + - description: literals + pipeline: + - Literals: + - Constant: + - 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: + - arrayValue: + values: + - 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/tests/unit/v1/test_pipeline.py b/tests/unit/v1/test_pipeline.py index 10509cafb..234610bad 100644 --- a/tests/unit/v1/test_pipeline.py +++ b/tests/unit/v1/test_pipeline.py @@ -392,6 +392,7 @@ def test_pipeline_execute_stream_equivalence(): ), ("replace_with", ("name",), stages.ReplaceWith), ("replace_with", (Field.of("n"),), stages.ReplaceWith), + ("literals", (Field.of("a"),), stages.Literals), ("sort", (Field.of("n").descending(),), stages.Sort), ("sort", (Field.of("n").descending(), Field.of("m").ascending()), stages.Sort), ("sample", (10,), stages.Sample), diff --git a/tests/unit/v1/test_pipeline_stages.py b/tests/unit/v1/test_pipeline_stages.py index a2d466f47..11a191e30 100644 --- a/tests/unit/v1/test_pipeline_stages.py +++ b/tests/unit/v1/test_pipeline_stages.py @@ -516,6 +516,32 @@ 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): + val = Constant.of({"a": 1}) + instance = self._make_one(val) + assert instance.documents == val + assert instance.name == "literals" + + def test_repr(self): + val = Constant.of({"a": 1}) + instance = self._make_one(val) + repr_str = repr(instance) + assert repr_str == "Literals(documents=Constant.of({'a': 1}))" + + def test_to_pb(self): + val = Constant.of({"a": 1}) + instance = self._make_one(val) + result = instance._to_pb() + assert result.name == "literals" + assert len(result.args) == 1 + assert result.args[0].map_value.fields["a"].integer_value == 1 + assert len(result.options) == 0 + + class TestOffset: def _make_one(self, *args, **kwargs): return stages.Offset(*args, **kwargs) From 513a4da078150f05653041abd1601ac248ff560c Mon Sep 17 00:00:00 2001 From: Linchin Date: Fri, 13 Feb 2026 23:58:53 +0000 Subject: [PATCH 2/4] fix system test --- google/cloud/firestore_v1/base_pipeline.py | 4 ++-- google/cloud/firestore_v1/pipeline_stages.py | 12 ++++++++--- tests/system/pipeline_e2e/general.yaml | 19 ++++++++---------- tests/unit/v1/test_pipeline_stages.py | 21 +++++++++++--------- 4 files changed, 31 insertions(+), 25 deletions(-) diff --git a/google/cloud/firestore_v1/base_pipeline.py b/google/cloud/firestore_v1/base_pipeline.py index 6dfcb39af..415a3dd9f 100644 --- a/google/cloud/firestore_v1/base_pipeline.py +++ b/google/cloud/firestore_v1/base_pipeline.py @@ -275,12 +275,12 @@ def find_nearest( def literals( self, - documents: Selectable, + *documents: Selectable, ) -> "_BasePipeline": """ TODO: add docstring. """ - return self._append(stages.Literals(documents)) + return self._append(stages.Literals(*documents)) def replace_with( self, diff --git a/google/cloud/firestore_v1/pipeline_stages.py b/google/cloud/firestore_v1/pipeline_stages.py index c0845071b..f75a4735e 100644 --- a/google/cloud/firestore_v1/pipeline_stages.py +++ b/google/cloud/firestore_v1/pipeline_stages.py @@ -345,12 +345,18 @@ def _pb_args(self): class Literals(Stage): """TODO: add docstring.""" - def __init__(self, documents: Selectable): + def __init__(self, *documents: Selectable): super().__init__("literals") - self.documents = Field(documents) if isinstance(documents, str) else documents + self.documents = documents def _pb_args(self): - return [self.documents._to_pb()] + 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): diff --git a/tests/system/pipeline_e2e/general.yaml b/tests/system/pipeline_e2e/general.yaml index f0e0d87d6..1ce427ffe 100644 --- a/tests/system/pipeline_e2e/general.yaml +++ b/tests/system/pipeline_e2e/general.yaml @@ -688,9 +688,8 @@ tests: - description: literals pipeline: - Literals: - - Constant: - - title: "The Hitchhiker's Guide to the Galaxy" - author: "Douglas Adams" + - title: "The Hitchhiker's Guide to the Galaxy" + author: "Douglas Adams" assert_results: - title: "The Hitchhiker's Guide to the Galaxy" author: "Douglas Adams" @@ -698,13 +697,11 @@ tests: pipeline: stages: - args: - - arrayValue: - values: - - mapValue: - fields: - author: - stringValue: "Douglas Adams" - title: - stringValue: "The Hitchhiker's Guide to the Galaxy" + - 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/tests/unit/v1/test_pipeline_stages.py b/tests/unit/v1/test_pipeline_stages.py index 11a191e30..58277298b 100644 --- a/tests/unit/v1/test_pipeline_stages.py +++ b/tests/unit/v1/test_pipeline_stages.py @@ -521,24 +521,27 @@ def _make_one(self, *args, **kwargs): return stages.Literals(*args, **kwargs) def test_ctor(self): - val = Constant.of({"a": 1}) - instance = self._make_one(val) - assert instance.documents == val + 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): - val = Constant.of({"a": 1}) - instance = self._make_one(val) + val1 = Constant.of({"a": 1}) + instance = self._make_one(val1) repr_str = repr(instance) - assert repr_str == "Literals(documents=Constant.of({'a': 1}))" + assert repr_str == "Literals(documents=(Constant.of({'a': 1}),))" def test_to_pb(self): - val = Constant.of({"a": 1}) - instance = self._make_one(val) + 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) == 1 + 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 From f2a39f044065145c5fcb468d644ff59407cf735d Mon Sep 17 00:00:00 2001 From: Linchin Date: Thu, 19 Feb 2026 00:04:30 +0000 Subject: [PATCH 3/4] add docstring --- google/cloud/firestore_v1/base_pipeline.py | 60 +++++++++++++++++++- google/cloud/firestore_v1/pipeline_stages.py | 2 +- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/google/cloud/firestore_v1/base_pipeline.py b/google/cloud/firestore_v1/base_pipeline.py index 415a3dd9f..70a0989d0 100644 --- a/google/cloud/firestore_v1/base_pipeline.py +++ b/google/cloud/firestore_v1/base_pipeline.py @@ -278,7 +278,65 @@ def literals( *documents: Selectable, ) -> "_BasePipeline": """ - TODO: add docstring. + 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: + >>> pipeline = client.pipeline() + ... .literals( + ... [ + ... {"name": "joe", "age": 10}, + ... {"name": "bob", "age": 30}, + ... {"name": "alice", "age": 40} + ... ] + ... ) + ... .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 + >>> pipeline = client.pipeline() + ... .literals( + ... [ + ... {"x": Constant.of("foo-bar-baz").char_length()}, + ... {"x": Constant.of("bar").char_length()} + ... ] + ... ) + + Output documents: + ```json + [ + {"x": 11}, + {"x": 3} + ] + ``` + + Args: + documents: A fixed set of predefined document objects. + Returns: + A new Pipeline object with this stage appended to the stage list """ return self._append(stages.Literals(*documents)) diff --git a/google/cloud/firestore_v1/pipeline_stages.py b/google/cloud/firestore_v1/pipeline_stages.py index f75a4735e..f65afcd41 100644 --- a/google/cloud/firestore_v1/pipeline_stages.py +++ b/google/cloud/firestore_v1/pipeline_stages.py @@ -343,7 +343,7 @@ def _pb_args(self): class Literals(Stage): - """TODO: add docstring.""" + """Returns documents from a fixed set of predefined document objects.""" def __init__(self, *documents: Selectable): super().__init__("literals") From d3796b0678587345fa40a3965c13d1429e4448b7 Mon Sep 17 00:00:00 2001 From: Linchin Date: Thu, 19 Feb 2026 00:31:43 +0000 Subject: [PATCH 4/4] improve docstring and type annotation --- google/cloud/firestore_v1/base_pipeline.py | 38 ++++++++++---------- google/cloud/firestore_v1/pipeline_stages.py | 2 +- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/google/cloud/firestore_v1/base_pipeline.py b/google/cloud/firestore_v1/base_pipeline.py index 70a0989d0..0613722c5 100644 --- a/google/cloud/firestore_v1/base_pipeline.py +++ b/google/cloud/firestore_v1/base_pipeline.py @@ -273,10 +273,7 @@ def find_nearest( stages.FindNearest(field, vector, distance_measure, options) ) - def literals( - self, - *documents: Selectable, - ) -> "_BasePipeline": + def literals(self, *documents: str | Selectable) -> "_BasePipeline": """ Returns documents from a fixed set of predefined document objects. @@ -284,14 +281,14 @@ def literals( 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( - ... [ - ... {"name": "joe", "age": 10}, - ... {"name": "bob", "age": 30}, - ... {"name": "alice", "age": 40} - ... ] - ... ) + ... .literals(Constant.of(documents)) ... .where(field("age").lessThan(35)) Output documents: @@ -317,13 +314,11 @@ def literals( Example: >>> from google.cloud.firestore_v1.pipeline_expressions import Constant - >>> pipeline = client.pipeline() - ... .literals( - ... [ - ... {"x": Constant.of("foo-bar-baz").char_length()}, - ... {"x": Constant.of("bar").char_length()} - ... ] - ... ) + >>> 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 @@ -334,9 +329,12 @@ def literals( ``` Args: - documents: A fixed set of predefined document objects. + 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 + A new Pipeline object with this stage appended to the stage list. """ return self._append(stages.Literals(*documents)) diff --git a/google/cloud/firestore_v1/pipeline_stages.py b/google/cloud/firestore_v1/pipeline_stages.py index f65afcd41..258a71b82 100644 --- a/google/cloud/firestore_v1/pipeline_stages.py +++ b/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): + def __init__(self, *documents: str | Selectable): super().__init__("literals") self.documents = documents