Skip to content

Commit 4615c41

Browse files
feat(firestore): add new pipeline expressions (#16151)
Adds the following expressions: - cmp - timestamp_trunc - timestamp_diff - timestamp_extract - if_null - type - is_type
1 parent 4e80530 commit 4615c41

File tree

7 files changed

+720
-20
lines changed

7 files changed

+720
-20
lines changed

packages/google-cloud-firestore/google/cloud/firestore_v1/pipeline_expressions.py

Lines changed: 265 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,75 @@
4848
)
4949

5050

51+
class TimeUnit(str, Enum):
52+
"""Enumeration of the different time units supported by the Firestore backend."""
53+
54+
MICROSECOND = "microsecond"
55+
MILLISECOND = "millisecond"
56+
SECOND = "second"
57+
MINUTE = "minute"
58+
HOUR = "hour"
59+
DAY = "day"
60+
61+
62+
class TimeGranularity(str, Enum):
63+
"""Enumeration of the different time granularities supported by the Firestore backend."""
64+
65+
# Inherit from TimeUnit
66+
MICROSECOND = TimeUnit.MICROSECOND.value
67+
MILLISECOND = TimeUnit.MILLISECOND.value
68+
SECOND = TimeUnit.SECOND.value
69+
MINUTE = TimeUnit.MINUTE.value
70+
HOUR = TimeUnit.HOUR.value
71+
DAY = TimeUnit.DAY.value
72+
73+
# Additional granularities
74+
WEEK = "week"
75+
WEEK_MONDAY = "week(monday)"
76+
WEEK_TUESDAY = "week(tuesday)"
77+
WEEK_WEDNESDAY = "week(wednesday)"
78+
WEEK_THURSDAY = "week(thursday)"
79+
WEEK_FRIDAY = "week(friday)"
80+
WEEK_SATURDAY = "week(saturday)"
81+
WEEK_SUNDAY = "week(sunday)"
82+
ISOWEEK = "isoweek"
83+
MONTH = "month"
84+
QUARTER = "quarter"
85+
YEAR = "year"
86+
ISOYEAR = "isoyear"
87+
88+
89+
class TimePart(str, Enum):
90+
"""Enumeration of the different time parts supported by the Firestore backend."""
91+
92+
# Inherit from TimeUnit
93+
MICROSECOND = TimeUnit.MICROSECOND.value
94+
MILLISECOND = TimeUnit.MILLISECOND.value
95+
SECOND = TimeUnit.SECOND.value
96+
MINUTE = TimeUnit.MINUTE.value
97+
HOUR = TimeUnit.HOUR.value
98+
DAY = TimeUnit.DAY.value
99+
100+
# Inherit from TimeGranularity
101+
WEEK = TimeGranularity.WEEK.value
102+
WEEK_MONDAY = TimeGranularity.WEEK_MONDAY.value
103+
WEEK_TUESDAY = TimeGranularity.WEEK_TUESDAY.value
104+
WEEK_WEDNESDAY = TimeGranularity.WEEK_WEDNESDAY.value
105+
WEEK_THURSDAY = TimeGranularity.WEEK_THURSDAY.value
106+
WEEK_FRIDAY = TimeGranularity.WEEK_FRIDAY.value
107+
WEEK_SATURDAY = TimeGranularity.WEEK_SATURDAY.value
108+
WEEK_SUNDAY = TimeGranularity.WEEK_SUNDAY.value
109+
ISOWEEK = TimeGranularity.ISOWEEK.value
110+
MONTH = TimeGranularity.MONTH.value
111+
QUARTER = TimeGranularity.QUARTER.value
112+
YEAR = TimeGranularity.YEAR.value
113+
ISOYEAR = TimeGranularity.ISOYEAR.value
114+
115+
# Additional parts
116+
DAY_OF_WEEK = "dayofweek"
117+
DAY_OF_YEAR = "dayofyear"
118+
119+
51120
class Ordering:
52121
"""Represents the direction for sorting results in a pipeline."""
53122

@@ -90,6 +159,31 @@ def _to_pb(self) -> Value:
90159
)
91160

92161

162+
class Type(str, Enum):
163+
"""Enumeration of the different types generated by the Firestore backend."""
164+
165+
NULL = "null"
166+
ARRAY = "array"
167+
BOOLEAN = "boolean"
168+
BYTES = "bytes"
169+
TIMESTAMP = "timestamp"
170+
GEO_POINT = "geo_point"
171+
NUMBER = "number"
172+
INT32 = "int32"
173+
INT64 = "int64"
174+
FLOAT64 = "float64"
175+
DECIMAL128 = "decimal128"
176+
MAP = "map"
177+
REFERENCE = "reference"
178+
STRING = "string"
179+
VECTOR = "vector"
180+
MAX_KEY = "max_key"
181+
MIN_KEY = "min_key"
182+
OBJECT_ID = "object_id"
183+
REGEX = "regex"
184+
REQUEST_TIMESTAMP = "request_timestamp"
185+
186+
93187
class Expression(ABC):
94188
"""Represents an expression that can be evaluated to a value within the
95189
execution of a pipeline.
@@ -120,6 +214,8 @@ def _cast_to_expr_or_convert_to_constant(
120214
"""Convert arbitrary object to an Expression."""
121215
if isinstance(o, Expression):
122216
return o
217+
if isinstance(o, Enum):
218+
o = o.value
123219
if isinstance(o, dict):
124220
return Map(o)
125221
if isinstance(o, list):
@@ -2079,20 +2175,21 @@ def unix_seconds_to_timestamp(self) -> "Expression":
20792175

20802176
@expose_as_static
20812177
def timestamp_add(
2082-
self, unit: Expression | str, amount: Expression | float
2178+
self, unit: TimeUnit | str | Expression, amount: Expression | float
20832179
) -> "Expression":
20842180
"""Creates an expression that adds a specified amount of time to this timestamp expression.
20852181
20862182
Example:
2183+
>>> # Add 1.5 days to the 'timestamp' field using TimeUnit enum.
2184+
>>> Field.of("timestamp").timestamp_add(TimeUnit.DAY, 1.5)
20872185
>>> # Add a duration specified by the 'unit' and 'amount' fields to the 'timestamp' field.
20882186
>>> Field.of("timestamp").timestamp_add(Field.of("unit"), Field.of("amount"))
2089-
>>> # Add 1.5 days to the 'timestamp' field.
2187+
>>> # Add 1.5 days to the 'timestamp' field using a string.
20902188
>>> Field.of("timestamp").timestamp_add("day", 1.5)
20912189
20922190
Args:
2093-
unit: The expression or string evaluating to the unit of time to add, must be one of
2094-
'microsecond', 'millisecond', 'second', 'minute', 'hour', 'day'.
2095-
amount: The expression or float representing the amount of time to add.
2191+
unit: The unit of time to add.
2192+
amount: The amount of time to add.
20962193
20972194
Returns:
20982195
A new `Expression` representing the resulting timestamp.
@@ -2108,20 +2205,21 @@ def timestamp_add(
21082205

21092206
@expose_as_static
21102207
def timestamp_subtract(
2111-
self, unit: Expression | str, amount: Expression | float
2208+
self, unit: TimeUnit | str | Expression, amount: Expression | float
21122209
) -> "Expression":
21132210
"""Creates an expression that subtracts a specified amount of time from this timestamp expression.
21142211
21152212
Example:
2213+
>>> # Subtract 2.5 hours from the 'timestamp' field using TimeUnit enum.
2214+
>>> Field.of("timestamp").timestamp_subtract(TimeUnit.HOUR, 2.5)
21162215
>>> # Subtract a duration specified by the 'unit' and 'amount' fields from the 'timestamp' field.
21172216
>>> Field.of("timestamp").timestamp_subtract(Field.of("unit"), Field.of("amount"))
2118-
>>> # Subtract 2.5 hours from the 'timestamp' field.
2217+
>>> # Subtract 2.5 hours from the 'timestamp' field using a string.
21192218
>>> Field.of("timestamp").timestamp_subtract("hour", 2.5)
21202219
21212220
Args:
2122-
unit: The expression or string evaluating to the unit of time to subtract, must be one of
2123-
'microsecond', 'millisecond', 'second', 'minute', 'hour', 'day'.
2124-
amount: The expression or float representing the amount of time to subtract.
2221+
unit: The unit of time to subtract.
2222+
amount: The amount of time to subtract.
21252223
21262224
Returns:
21272225
A new `Expression` representing the resulting timestamp.
@@ -2206,6 +2304,163 @@ def as_(self, alias: str) -> "AliasedExpression":
22062304
"""
22072305
return AliasedExpression(self, alias)
22082306

2307+
@expose_as_static
2308+
def cmp(self, other: Expression | CONSTANT_TYPE) -> "Expression":
2309+
"""Creates an expression that compares this expression to another expression.
2310+
2311+
Returns an integer:
2312+
* -1 if this expression is less than the other
2313+
* 0 if they are equal
2314+
* 1 if this expression is greater than the other
2315+
2316+
Example:
2317+
>>> # Compare the 'price' field to 10
2318+
>>> Field.of("price").cmp(10)
2319+
2320+
Args:
2321+
other: The value to compare against.
2322+
2323+
Returns:
2324+
A new `Expression` representing the comparison operation.
2325+
"""
2326+
return FunctionExpression(
2327+
"cmp", [self, self._cast_to_expr_or_convert_to_constant(other)]
2328+
)
2329+
2330+
@expose_as_static
2331+
def timestamp_trunc(
2332+
self,
2333+
granularity: TimeGranularity | Expression | str,
2334+
timezone: Expression | str | None = None,
2335+
) -> "Expression":
2336+
"""Creates an expression that truncates a timestamp to a specified granularity.
2337+
2338+
Example:
2339+
>>> # Truncate the 'createdAt' field to the day using TimeGranularity enum
2340+
>>> Field.of("createdAt").timestamp_trunc(TimeGranularity.DAY)
2341+
>>> # Truncate the 'createdAt' field to the day in the 'America/Los_Angeles' timezone
2342+
>>> Field.of("createdAt").timestamp_trunc(TimeGranularity.DAY, "America/Los_Angeles")
2343+
>>> # Truncate the 'createdAt' field to the day using a string
2344+
>>> Field.of("createdAt").timestamp_trunc("day")
2345+
2346+
Args:
2347+
granularity: The granularity to truncate to.
2348+
timezone: The optional timezone.
2349+
2350+
Returns:
2351+
A new `Expression` representing the timestamp_trunc operation.
2352+
"""
2353+
args = [self, self._cast_to_expr_or_convert_to_constant(granularity)]
2354+
if timezone is not None:
2355+
args.append(self._cast_to_expr_or_convert_to_constant(timezone))
2356+
return FunctionExpression("timestamp_trunc", args)
2357+
2358+
@expose_as_static
2359+
def timestamp_extract(
2360+
self,
2361+
part: TimePart | str | Expression,
2362+
timezone: str | Expression | None = None,
2363+
) -> "Expression":
2364+
"""Creates an expression that extracts a part of a timestamp.
2365+
2366+
Example:
2367+
>>> # Extract the year from the 'createdAt' field using TimePart enum
2368+
>>> Field.of("createdAt").timestamp_extract(TimePart.YEAR)
2369+
>>> # Extract the year from the 'createdAt' field in the 'America/Los_Angeles' timezone
2370+
>>> Field.of("createdAt").timestamp_extract(TimePart.YEAR, "America/Los_Angeles")
2371+
>>> # Extract the year from the 'createdAt' field using a string
2372+
>>> Field.of("createdAt").timestamp_extract("year")
2373+
2374+
Args:
2375+
part: The part to extract.
2376+
timezone: The optional timezone.
2377+
2378+
Returns:
2379+
A new `Expression` representing the timestamp_extract operation.
2380+
"""
2381+
args = [self, self._cast_to_expr_or_convert_to_constant(part)]
2382+
if timezone is not None:
2383+
args.append(self._cast_to_expr_or_convert_to_constant(timezone))
2384+
return FunctionExpression("timestamp_extract", args)
2385+
2386+
@expose_as_static
2387+
def timestamp_diff(
2388+
self, start: Expression | datetime.datetime, unit: TimeUnit | str | Expression
2389+
) -> "Expression":
2390+
"""Creates an expression that computes the difference between two timestamps in the specified unit.
2391+
2392+
Example:
2393+
>>> # Compute the difference in days between the 'end' field and the 'start' field using TimeUnit enum
2394+
>>> Field.of("end").timestamp_diff(Field.of("start"), TimeUnit.DAY)
2395+
>>> # Compute the difference in days using a string
2396+
>>> Field.of("end").timestamp_diff(Field.of("start"), "day")
2397+
2398+
Args:
2399+
start: The start timestamp.
2400+
unit: The unit of time.
2401+
2402+
Returns:
2403+
A new `Expression` representing the timestamp_diff operation.
2404+
"""
2405+
return FunctionExpression(
2406+
"timestamp_diff",
2407+
[
2408+
self,
2409+
self._cast_to_expr_or_convert_to_constant(start),
2410+
self._cast_to_expr_or_convert_to_constant(unit),
2411+
],
2412+
)
2413+
2414+
@expose_as_static
2415+
def if_null(self, *others: Expression | CONSTANT_TYPE) -> "Expression":
2416+
"""Creates an expression that returns the first non-null expression from the provided arguments.
2417+
2418+
Example:
2419+
>>> # Return the 'nickname' field if not null, otherwise return 'firstName'
2420+
>>> Field.of("nickname").if_null(Field.of("firstName"))
2421+
2422+
Args:
2423+
*others: Additional expressions or constants to evaluate if the previous ones are null.
2424+
2425+
Returns:
2426+
A new `Expression` representing the if_null operation.
2427+
"""
2428+
return FunctionExpression(
2429+
"if_null",
2430+
[self] + [self._cast_to_expr_or_convert_to_constant(o) for o in others],
2431+
)
2432+
2433+
@expose_as_static
2434+
def type(self) -> "Expression":
2435+
"""Creates an expression that returns the data type of this expression's result as a string.
2436+
2437+
Example:
2438+
>>> # Get the type of the 'title' field
2439+
>>> Field.of("title").type()
2440+
2441+
Returns:
2442+
A new `Expression` representing the type operation.
2443+
"""
2444+
return FunctionExpression("type", [self])
2445+
2446+
@expose_as_static
2447+
def is_type(self, type_val: Type | str | Expression) -> "BooleanExpression":
2448+
"""Creates an expression that checks if the result is of the specified type.
2449+
2450+
Example:
2451+
>>> # Check if the 'price' field is a number
2452+
>>> Field.of("price").is_type("number")
2453+
2454+
Args:
2455+
type_val: The type string or expression to check against.
2456+
2457+
Returns:
2458+
A new `BooleanExpression` representing the is_type operation.
2459+
"""
2460+
return BooleanExpression(
2461+
"is_type", [self, self._cast_to_expr_or_convert_to_constant(type_val)]
2462+
)
2463+
22092464

22102465
class Constant(Expression, Generic[CONSTANT_TYPE]):
22112466
"""Represents a constant literal value in an expression."""

0 commit comments

Comments
 (0)