Skip to content

Commit cf50cea

Browse files
authored
feat(firestore): literals pipeline stage (#16028)
Succeeding googleapis/python-firestore#1170 for the monorepo migration.
1 parent 4b400fa commit cf50cea

5 files changed

Lines changed: 263 additions & 3 deletions

File tree

‎packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_source.py‎

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
from __future__ importannotations
2121

22-
fromtypingimportTYPE_CHECKING, Generic, TypeVar
22+
fromtypingimportGeneric, TypeVar, TYPE_CHECKING
2323

2424
fromgoogle.cloud.firestore_v1importpipeline_stagesasstages
2525
fromgoogle.cloud.firestore_v1._helpersimportDOCUMENT_PATH_DELIMITER
@@ -32,6 +32,7 @@
3232
fromgoogle.cloud.firestore_v1.base_documentimportBaseDocumentReference
3333
fromgoogle.cloud.firestore_v1.base_queryimportBaseQuery
3434
fromgoogle.cloud.firestore_v1.clientimportClient
35+
fromgoogle.cloud.firestore_v1.pipeline_expressionsimportCONSTANT_TYPE, Expression
3536

3637

3738
PipelineType=TypeVar("PipelineType", bound=_BasePipeline)
@@ -108,3 +109,65 @@ def documents(self, *docs: "BaseDocumentReference") -> PipelineType:
108109
a new pipeline instance targeting the specified documents
109110
"""
110111
returnself._create_pipeline(stages.Documents.of(*docs))
112+
113+
defliterals(
114+
self, *documents: dict[str, Expression|CONSTANT_TYPE]
115+
) ->PipelineType:
116+
"""
117+
Returns documents from a fixed set of predefined document objects.
118+
119+
Example:
120+
>>> from google.cloud.firestore_v1.pipeline_expressions import Constant
121+
>>> documents = [
122+
... {"name": "joe", "age": 10},
123+
... {"name": "bob", "age": 30},
124+
... {"name": "alice", "age": 40}
125+
... ]
126+
>>> pipeline = client.pipeline()
127+
... .literals(*documents)
128+
... .where(field("age").lessThan(35))
129+
130+
Output documents:
131+
```json
132+
[
133+
{"name": "joe", "age": 10},
134+
{"name": "bob", "age": 30}
135+
]
136+
```
137+
138+
Behavior:
139+
The `literals(...)` stage can only be used as the first stage in a pipeline (or
140+
sub-pipeline). The order of documents returned from the `literals` matches the
141+
order in which they are defined.
142+
143+
While literal values are the most common, it is also possible to pass in
144+
expressions, which will be evaluated and returned, making it possible to test
145+
out different query / expression behavior without first needing to create some
146+
test data.
147+
148+
For example, the following shows how to quickly test out the `length(...)`
149+
function on some constant test sets:
150+
151+
Example:
152+
>>> from google.cloud.firestore_v1.pipeline_expressions import Constant
153+
>>> documents = [
154+
... {"x": Constant.of("foo-bar-baz").char_length()},
155+
... {"x": Constant.of("bar").char_length()}
156+
... ]
157+
>>> pipeline = client.pipeline().literals(*documents)
158+
159+
Output documents:
160+
```json
161+
[
162+
{"x": 11},
163+
{"x": 3}
164+
]
165+
```
166+
167+
Args:
168+
*documents: One or more documents to be returned by this stage. Each can be a `dict`
169+
of values of `Expression` or `CONSTANT_TYPE` types.
170+
Returns:
171+
A new Pipeline object with this stage appended to the stage list.
172+
"""
173+
returnself._create_pipeline(stages.Literals(*documents))

‎packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_stages.py‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@
2121

2222
fromabcimportABC, abstractmethod
2323
fromenumimportEnum
24-
fromtypingimportTYPE_CHECKING, Optional, Sequence
24+
fromtypingimportTYPE_CHECKING, Any, Mapping, Optional, Sequence
2525

2626
fromgoogle.cloud.firestore_v1._helpersimportencode_value
2727
fromgoogle.cloud.firestore_v1.base_vector_queryimportDistanceMeasure
2828
fromgoogle.cloud.firestore_v1.pipeline_expressionsimport (
2929
AggregateFunction,
3030
AliasedExpression,
3131
BooleanExpression,
32+
CONSTANT_TYPE,
3233
Expression,
3334
Field,
3435
Ordering,
@@ -342,6 +343,26 @@ def _pb_args(self):
342343
return [Value(integer_value=self.limit)]
343344

344345

346+
classLiterals(Stage):
347+
"""Returns documents from a fixed set of predefined document objects."""
348+
349+
def__init__(self, *documents: dict[str, Expression|CONSTANT_TYPE]):
350+
super().__init__("literals")
351+
self.documents: tuple[Mapping[str, Any], ...] =documents
352+
353+
def_pb_args(self):
354+
args= []
355+
fordocinself.documents:
356+
encoded_doc= {}
357+
fork, vindoc.items():
358+
ifhasattr(v, "_to_pb"):
359+
encoded_doc[k] =v._to_pb()
360+
else:
361+
encoded_doc[k] =encode_value(v)
362+
args.append(Value(map_value={"fields": encoded_doc}))
363+
returnargs
364+
365+
345366
classOffset(Stage):
346367
"""Skips a specified number of documents."""
347368

‎packages/google-cloud-firestore/tests/system/pipeline_e2e/general.yaml‎

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -684,4 +684,63 @@ tests:
684684
- args:
685685
- fieldReferenceValue: awards
686686
- stringValue: full_replace
687-
name: replace_with
687+
name: replace_with
688+
- description: literals
689+
pipeline:
690+
- Literals:
691+
- title: "The Hitchhiker's Guide to the Galaxy"
692+
author: "Douglas Adams"
693+
- genre: "Science Fiction"
694+
year: 1979
695+
assert_results:
696+
- title: "The Hitchhiker's Guide to the Galaxy"
697+
author: "Douglas Adams"
698+
- genre: "Science Fiction"
699+
year: 1979
700+
assert_proto:
701+
pipeline:
702+
stages:
703+
- args:
704+
- mapValue:
705+
fields:
706+
author:
707+
stringValue: "Douglas Adams"
708+
title:
709+
stringValue: "The Hitchhiker's Guide to the Galaxy"
710+
- mapValue:
711+
fields:
712+
genre:
713+
stringValue: "Science Fiction"
714+
year:
715+
integerValue: '1979'
716+
name: literals
717+
- description: literals_with_expression_input
718+
pipeline:
719+
- Literals:
720+
- res:
721+
FunctionExpression.string_concat:
722+
- Constant: "A"
723+
- Constant: "B"
724+
- Select:
725+
- res
726+
assert_results:
727+
- res: "AB"
728+
assert_proto:
729+
pipeline:
730+
stages:
731+
- args:
732+
- mapValue:
733+
fields:
734+
res:
735+
functionValue:
736+
args:
737+
- stringValue: "A"
738+
- stringValue: "B"
739+
name: string_concat
740+
name: literals
741+
- args:
742+
- mapValue:
743+
fields:
744+
res:
745+
fieldReferenceValue: res
746+
name: select

‎packages/google-cloud-firestore/tests/unit/v1/test_pipeline_source.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,17 @@ def test_documents(self):
110110
assertfirst_stage.paths[1] =="/a/2"
111111
assertfirst_stage.paths[2] =="/a/3"
112112

113+
deftest_literals(self):
114+
fromgoogle.cloud.firestore_v1.pipeline_expressionsimportField
115+
116+
instance=self._make_client().pipeline()
117+
documents= ({"field": Field.of("a")}, {"name": "joe"})
118+
ppl=instance.literals(*documents)
119+
assertisinstance(ppl, self._expected_pipeline_type)
120+
assertlen(ppl.stages) ==1
121+
first_stage=ppl.stages[0]
122+
assertisinstance(first_stage, stages.Literals)
123+
113124

114125
classTestPipelineSourceWithAsyncClient(TestPipelineSource):
115126
"""

‎packages/google-cloud-firestore/tests/unit/v1/test_pipeline_stages.py‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,112 @@ def test_to_pb(self):
517517
assertlen(result.options) ==0
518518

519519

520+
classTestLiterals:
521+
def_make_one(self, *args, **kwargs):
522+
returnstages.Literals(*args, **kwargs)
523+
524+
deftest_ctor(self):
525+
val1= {"a": Field.of("a")}
526+
val2= {"b": 2}
527+
instance=self._make_one(val1, val2)
528+
assertinstance.documents== (val1, val2)
529+
assertinstance.name=="literals"
530+
531+
deftest_ctor_extended_types(self):
532+
importdatetime
533+
fromgoogle.cloud.firestore_v1._helpersimportGeoPoint
534+
fromgoogle.cloud.firestore_v1.vectorimportVector
535+
536+
doc= {
537+
"a": 1,
538+
"b": "string",
539+
"c": 3.14,
540+
"d": True,
541+
"e": None,
542+
"f": b"bytes",
543+
"g": datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc),
544+
"h": GeoPoint(1.0, 2.0),
545+
"i": Vector([1.0, 2.0]),
546+
}
547+
instance=self._make_one(doc)
548+
assertinstance.documents== (doc,)
549+
assertinstance.name=="literals"
550+
551+
deftest_ctor_w_expressions(self):
552+
fromgoogle.cloud.firestore_v1.pipeline_expressionsimportFunctionExpression
553+
554+
expr=FunctionExpression("string_concat", [Constant("A"), Constant("B")])
555+
doc= {"res": expr}
556+
instance=self._make_one(doc)
557+
assertinstance.documents== (doc,)
558+
assertinstance.name=="literals"
559+
560+
deftest_repr(self):
561+
fromgoogle.cloud.firestore_v1.pipeline_expressionsimportField
562+
563+
val1= {"a": Field.of("a")}
564+
instance=self._make_one(val1, {"b": 2})
565+
repr_str=repr(instance)
566+
assertrepr_str=="Literals(documents=({'a': Field.of('a')}, {'b': 2}))"
567+
568+
deftest_to_pb_constant_types(self):
569+
importdatetime
570+
fromgoogle.cloud.firestore_v1._helpersimportGeoPoint
571+
fromgoogle.cloud.firestore_v1.vectorimportVector
572+
573+
doc= {
574+
"a": 1,
575+
"b": "string",
576+
"c": 3.14,
577+
"d": True,
578+
"e": None,
579+
"f": b"bytes",
580+
"g": datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc),
581+
"h": GeoPoint(1.0, 2.0),
582+
"i": Vector([1.0, 2.0]),
583+
}
584+
instance=self._make_one(doc)
585+
result=instance._to_pb()
586+
assertresult.name=="literals"
587+
assertlen(result.args) ==1
588+
589+
fields=result.args[0].map_value.fields
590+
assertfields["a"].integer_value==1
591+
assertfields["b"].string_value=="string"
592+
assertfields["c"].double_value==3.14
593+
assertfields["d"].boolean_valueisTrue
594+
assertfields["e"].null_value==0
595+
assertfields["f"].bytes_value==b"bytes"
596+
assertfields["g"].timestamp_value==datetime.datetime(
597+
2025, 1, 1, tzinfo=datetime.timezone.utc
598+
)
599+
assertfields["h"].geo_point_value.latitude==1.0
600+
assertfields["h"].geo_point_value.longitude==2.0
601+
assert (
602+
fields["i"].map_value.fields["value"].array_value.values[0].double_value
603+
==1.0
604+
)
605+
assert (
606+
fields["i"].map_value.fields["value"].array_value.values[1].double_value
607+
==2.0
608+
)
609+
610+
deftest_to_pb_w_expression(self):
611+
fromgoogle.cloud.firestore_v1.pipeline_expressionsimportFunctionExpression
612+
613+
expr=FunctionExpression("string_concat", [Constant("A"), Constant("B")])
614+
doc= {"res": expr}
615+
instance=self._make_one(doc)
616+
result=instance._to_pb()
617+
assertresult.name=="literals"
618+
assertlen(result.args) ==1
619+
620+
fields=result.args[0].map_value.fields
621+
assertfields["res"].function_value.name=="string_concat"
622+
assertfields["res"].function_value.args[0].string_value=="A"
623+
assertfields["res"].function_value.args[1].string_value=="B"
624+
625+
520626
classTestOffset:
521627
def_make_one(self, *args, **kwargs):
522628
returnstages.Offset(*args, **kwargs)

0 commit comments

Comments
 (0)