Skip to content
This repository was archived by the owner on Mar 2, 2026. It is now read-only.

Commit 8e83a40

Browse files
chore: remove is_nan and is_null (#1123)
1 parent 9a35dfe commit 8e83a40

8 files changed

Lines changed: 61 additions & 165 deletions

File tree

google/cloud/firestore_v1/pipeline_expressions.py

Lines changed: 8 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -816,56 +816,6 @@ def if_absent(self, default_value: Expression | CONSTANT_TYPE) -> "Expression":
816816
[self, self._cast_to_expr_or_convert_to_constant(default_value)],
817817
)
818818

819-
@expose_as_static
820-
def is_nan(self) -> "BooleanExpression":
821-
"""Creates an expression that checks if this expression evaluates to 'NaN' (Not a Number).
822-
823-
Example:
824-
>>> # Check if the result of a calculation is NaN
825-
>>> Field.of("value").divide(0).is_nan()
826-
827-
Returns:
828-
A new `Expression` representing the 'isNaN' check.
829-
"""
830-
return BooleanExpression("is_nan", [self])
831-
832-
@expose_as_static
833-
def is_not_nan(self) -> "BooleanExpression":
834-
"""Creates an expression that checks if this expression evaluates to a non-'NaN' (Not a Number) value.
835-
836-
Example:
837-
>>> # Check if the result of a calculation is not NaN
838-
>>> Field.of("value").divide(1).is_not_nan()
839-
840-
Returns:
841-
A new `Expression` representing the 'is not NaN' check.
842-
"""
843-
return BooleanExpression("is_not_nan", [self])
844-
845-
@expose_as_static
846-
def is_null(self) -> "BooleanExpression":
847-
"""Creates an expression that checks if the value of a field is 'Null'.
848-
849-
Example:
850-
>>> Field.of("value").is_null()
851-
852-
Returns:
853-
A new `Expression` representing the 'isNull' check.
854-
"""
855-
return BooleanExpression("is_null", [self])
856-
857-
@expose_as_static
858-
def is_not_null(self) -> "BooleanExpression":
859-
"""Creates an expression that checks if the value of a field is not 'Null'.
860-
861-
Example:
862-
>>> Field.of("value").is_not_null()
863-
864-
Returns:
865-
A new `Expression` representing the 'isNotNull' check.
866-
"""
867-
return BooleanExpression("is_not_null", [self])
868-
869819
@expose_as_static
870820
def is_error(self):
871821
"""Creates an expression that checks if a given expression produces an error
@@ -1653,7 +1603,10 @@ def of(value: CONSTANT_TYPE) -> Constant[CONSTANT_TYPE]:
16531603
return Constant(value)
16541604

16551605
def __repr__(self):
1656-
return f"Constant.of({self.value!r})"
1606+
value_str = repr(self.value)
1607+
if isinstance(self.value, float) and value_str == "nan":
1608+
value_str = "math.nan"
1609+
return f"Constant.of({value_str})"
16571610

16581611
def __hash__(self):
16591612
return hash(self.value)
@@ -1827,13 +1780,13 @@ def _from_query_filter_pb(filter_pb, client):
18271780
elif isinstance(filter_pb, Query_pb.UnaryFilter):
18281781
field = Field.of(filter_pb.field.field_path)
18291782
if filter_pb.op == Query_pb.UnaryFilter.Operator.IS_NAN:
1830-
return And(field.exists(), field.is_nan())
1783+
return And(field.exists(), field.equal(float("nan")))
18311784
elif filter_pb.op == Query_pb.UnaryFilter.Operator.IS_NOT_NAN:
1832-
return And(field.exists(), field.is_not_nan())
1785+
return And(field.exists(), Not(field.equal(float("nan"))))
18331786
elif filter_pb.op == Query_pb.UnaryFilter.Operator.IS_NULL:
1834-
return And(field.exists(), field.is_null())
1787+
return And(field.exists(), field.equal(None))
18351788
elif filter_pb.op == Query_pb.UnaryFilter.Operator.IS_NOT_NULL:
1836-
return And(field.exists(), field.is_not_null())
1789+
return And(field.exists(), Not(field.equal(None)))
18371790
else:
18381791
raise TypeError(f"Unexpected UnaryFilter operator type: {filter_pb.op}")
18391792
elif isinstance(filter_pb, Query_pb.FieldFilter):

tests/system/pipeline_e2e/array.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ tests:
185185
- AliasedExpression:
186186
- Function.array_concat:
187187
- Field: tags
188-
- Constant: ["new_tag", "another_tag"]
188+
- ["new_tag", "another_tag"]
189189
- "concatenatedTags"
190190
assert_results:
191191
- concatenatedTags:
@@ -232,8 +232,8 @@ tests:
232232
- AliasedExpression:
233233
- Function.array_concat:
234234
- Field: tags
235-
- Constant: ["sci-fi"]
236-
- Constant: ["classic", "epic"]
235+
- ["sci-fi"]
236+
- ["classic", "epic"]
237237
- "concatenatedTags"
238238
assert_results:
239239
- concatenatedTags:

tests/system/pipeline_e2e/data.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,9 @@ data:
139139
vec3:
140140
embedding: [5.0, 6.0, 7.0]
141141
vec4:
142-
embedding: [1.0, 2.0, 4.0]
142+
embedding: [1.0, 2.0, 4.0]
143+
errors:
144+
doc_with_nan:
145+
value: "NaN"
146+
doc_with_null:
147+
value: null

tests/system/pipeline_e2e/logical.yaml

Lines changed: 20 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -238,89 +238,48 @@ tests:
238238
expression:
239239
fieldReferenceValue: title
240240
name: sort
241-
- description: testChecks
241+
- description: testIsNull
242242
pipeline:
243-
- Collection: books
243+
- Collection: errors
244244
- Where:
245-
- Not:
246-
- Function.is_nan:
247-
- Field: rating
248-
- Select:
249-
- AliasedExpression:
250-
- Not:
251-
- Function.is_nan:
252-
- Field: rating
253-
- "ratingIsNotNaN"
254-
- Limit: 1
245+
- Function.equal:
246+
- Field: value
247+
- null
255248
assert_results:
256-
- ratingIsNotNaN: true
249+
- value: null
257250
assert_proto:
258251
pipeline:
259252
stages:
260253
- args:
261-
- referenceValue: /books
254+
- referenceValue: /errors
262255
name: collection
263256
- args:
264257
- functionValue:
265258
args:
266-
- functionValue:
267-
args:
268-
- fieldReferenceValue: rating
269-
name: is_nan
270-
name: not
271-
name: where
272-
- args:
273-
- mapValue:
274-
fields:
275-
ratingIsNotNaN:
276-
functionValue:
277-
args:
278-
- functionValue:
279-
args:
280-
- fieldReferenceValue: rating
281-
name: is_nan
282-
name: not
283-
name: select
284-
- args:
285-
- integerValue: '1'
286-
name: limit
287-
- description: testIsNotNull
288-
pipeline:
289-
- Collection: books
290-
- Where:
291-
- Function.is_not_null:
292-
- Field: rating
293-
assert_count: 10
294-
assert_proto:
295-
pipeline:
296-
stages:
297-
- args:
298-
- referenceValue: /books
299-
name: collection
300-
- args:
301-
- functionValue:
302-
args:
303-
- fieldReferenceValue: rating
304-
name: is_not_null
259+
- fieldReferenceValue: value
260+
- nullValue: null
261+
name: equal
305262
name: where
306-
- description: testIsNotNaN
263+
- description: testIsNan
307264
pipeline:
308-
- Collection: books
265+
- Collection: errors
309266
- Where:
310-
- Function.is_not_nan:
311-
- Field: rating
312-
assert_count: 10
267+
- Function.equal:
268+
- Field: value
269+
- NaN
270+
assert_count: 1
313271
assert_proto:
314272
pipeline:
315273
stages:
316274
- args:
317-
- referenceValue: /books
275+
- referenceValue: /errors
318276
name: collection
319277
- args:
320278
- functionValue:
321279
args:
322-
- fieldReferenceValue: rating
323-
name: is_not_nan
280+
- fieldReferenceValue: value
281+
- doubleValue: NaN
282+
name: equal
324283
name: where
325284
- description: testIsAbsent
326285
pipeline:

tests/system/test_pipeline_acceptance.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,8 @@ def _parse_expressions(client, yaml_element: Any):
270270
}
271271
elif _is_expr_string(yaml_element):
272272
return getattr(pipeline_expressions, yaml_element)()
273+
elif yaml_element == "NaN":
274+
return float(yaml_element)
273275
else:
274276
return yaml_element
275277

@@ -351,6 +353,8 @@ def _parse_yaml_types(data):
351353
return parsed_datetime
352354
except ValueError:
353355
pass
356+
if data == "NaN":
357+
return float("NaN")
354358
return data
355359

356360

tests/system/test_system.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1727,7 +1727,7 @@ def test_query_with_order_dot_key(client, cleanup, database):
17271727
assert found_data == [snap.to_dict() for snap in cursor_with_key_data]
17281728

17291729

1730-
@pytest.mark.parametrize("database", TEST_DATABASES_W_ENTERPRISE, indirect=True)
1730+
@pytest.mark.parametrize("database", TEST_DATABASES, indirect=True)
17311731
def test_query_unary(client, cleanup, database):
17321732
collection_name = "unary" + UNIQUE_RESOURCE_ID
17331733
collection = client.collection(collection_name)
@@ -3287,7 +3287,7 @@ def test_query_with_or_composite_filter(collection, database):
32873287
verify_pipeline(query)
32883288

32893289

3290-
@pytest.mark.parametrize("database", TEST_DATABASES_W_ENTERPRISE, indirect=True)
3290+
@pytest.mark.parametrize("database", TEST_DATABASES, indirect=True)
32913291
@pytest.mark.parametrize(
32923292
"aggregation_type,expected_value", [("count", 5), ("sum", 100), ("avg", 4.0)]
32933293
)

tests/system/test_system_async.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1651,7 +1651,7 @@ async def test_query_with_order_dot_key(client, cleanup, database):
16511651
assert found_data == [snap.to_dict() for snap in cursor_with_key_data]
16521652

16531653

1654-
@pytest.mark.parametrize("database", TEST_DATABASES_W_ENTERPRISE, indirect=True)
1654+
@pytest.mark.parametrize("database", TEST_DATABASES, indirect=True)
16551655
async def test_query_unary(client, cleanup, database):
16561656
collection_name = "unary" + UNIQUE_RESOURCE_ID
16571657
collection = client.collection(collection_name)

tests/unit/v1/test_pipeline_expressions.py

Lines changed: 17 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import pytest
1616
import mock
17+
import math
1718
import datetime
1819

1920
from google.cloud.firestore_v1 import _helpers
@@ -117,6 +118,13 @@ def test_to_pb(self, input_val, to_pb_val):
117118
instance = Constant.of(input_val)
118119
assert instance._to_pb() == to_pb_val
119120

121+
@pytest.mark.parametrize("input", [float("nan"), math.nan])
122+
def test_nan_to_pb(self, input):
123+
instance = Constant.of(input)
124+
assert repr(instance) == "Constant.of(math.nan)"
125+
pb_val = instance._to_pb()
126+
assert math.isnan(pb_val.double_value)
127+
120128
@pytest.mark.parametrize(
121129
"input_val,expected",
122130
[
@@ -284,7 +292,7 @@ def test__from_query_filter_pb_composite_filter_or(self, mock_client):
284292
field1 = Field.of("field1")
285293
field2 = Field.of("field2")
286294
expected_cond1 = expr.And(field1.exists(), field1.equal(Constant("val1")))
287-
expected_cond2 = expr.And(field2.exists(), field2.is_null())
295+
expected_cond2 = expr.And(field2.exists(), field2.equal(None))
288296
expected = expr.Or(expected_cond1, expected_cond2)
289297

290298
assert repr(result) == repr(expected)
@@ -371,7 +379,7 @@ def test__from_query_filter_pb_composite_filter_nested(self, mock_client):
371379
field3 = Field.of("field3")
372380
expected_cond1 = expr.And(field1.exists(), field1.equal(Constant("val1")))
373381
expected_cond2 = expr.And(field2.exists(), field2.greater_than(Constant(10)))
374-
expected_cond3 = expr.And(field3.exists(), field3.is_not_null())
382+
expected_cond3 = expr.And(field3.exists(), expr.Not(field3.equal(None)))
375383
expected_inner_and = expr.And(expected_cond2, expected_cond3)
376384
expected_outer_or = expr.Or(expected_cond1, expected_inner_and)
377385

@@ -400,18 +408,21 @@ def test__from_query_filter_pb_composite_filter_unknown_op(self, mock_client):
400408
@pytest.mark.parametrize(
401409
"op_enum, expected_expr_func",
402410
[
403-
(query_pb.StructuredQuery.UnaryFilter.Operator.IS_NAN, Expression.is_nan),
411+
(
412+
query_pb.StructuredQuery.UnaryFilter.Operator.IS_NAN,
413+
lambda x: x.equal(float("nan")),
414+
),
404415
(
405416
query_pb.StructuredQuery.UnaryFilter.Operator.IS_NOT_NAN,
406-
Expression.is_not_nan,
417+
lambda x: expr.Not(x.equal(float("nan"))),
407418
),
408419
(
409420
query_pb.StructuredQuery.UnaryFilter.Operator.IS_NULL,
410-
Expression.is_null,
421+
lambda x: x.equal(None),
411422
),
412423
(
413424
query_pb.StructuredQuery.UnaryFilter.Operator.IS_NOT_NULL,
414-
Expression.is_not_null,
425+
lambda x: expr.Not(x.equal(None)),
415426
),
416427
],
417428
)
@@ -846,42 +857,6 @@ def test_if_absent(self):
846857
infix_instance = arg1.if_absent(arg2)
847858
assert infix_instance == instance
848859

849-
def test_is_nan(self):
850-
arg1 = self._make_arg("Value")
851-
instance = Expression.is_nan(arg1)
852-
assert instance.name == "is_nan"
853-
assert instance.params == [arg1]
854-
assert repr(instance) == "Value.is_nan()"
855-
infix_instance = arg1.is_nan()
856-
assert infix_instance == instance
857-
858-
def test_is_not_nan(self):
859-
arg1 = self._make_arg("Value")
860-
instance = Expression.is_not_nan(arg1)
861-
assert instance.name == "is_not_nan"
862-
assert instance.params == [arg1]
863-
assert repr(instance) == "Value.is_not_nan()"
864-
infix_instance = arg1.is_not_nan()
865-
assert infix_instance == instance
866-
867-
def test_is_null(self):
868-
arg1 = self._make_arg("Value")
869-
instance = Expression.is_null(arg1)
870-
assert instance.name == "is_null"
871-
assert instance.params == [arg1]
872-
assert repr(instance) == "Value.is_null()"
873-
infix_instance = arg1.is_null()
874-
assert infix_instance == instance
875-
876-
def test_is_not_null(self):
877-
arg1 = self._make_arg("Value")
878-
instance = Expression.is_not_null(arg1)
879-
assert instance.name == "is_not_null"
880-
assert instance.params == [arg1]
881-
assert repr(instance) == "Value.is_not_null()"
882-
infix_instance = arg1.is_not_null()
883-
assert infix_instance == instance
884-
885860
def test_is_error(self):
886861
arg1 = self._make_arg("Value")
887862
instance = Expression.is_error(arg1)

0 commit comments

Comments
 (0)