Repository: googleapis/python-spanner-django

Issue:
django: DecimalField lookups crash
Example failure:
```
======================================================================
ERROR: test_gt (lookup.test_decimalfield.DecimalFieldLookupTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tim/code/django/tests/lookup/test_decimalfield.py", line 26, in test_gt
    self.assertCountEqual(qs, [self.p3])
  File "/opt/python3.7.0/lib/python3.7/unittest/case.py", line 1182, in assertCountEqual
    first_seq, second_seq = list(first), list(second)
  File "/home/tim/code/django/django/db/models/query.py", line 274, in __iter__
    self._fetch_all()
  File "/home/tim/code/django/django/db/models/query.py", line 1242, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
  File "/home/tim/code/django/django/db/models/query.py", line 55, in __iter__
    results = compiler.execute_sql(chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size)
  File "/home/tim/code/django/django/db/models/sql/compiler.py", line 1133, in execute_sql
    cursor.execute(sql, params)
  File "/home/tim/code/django/django/db/backends/utils.py", line 67, in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
  File "/home/tim/code/django/django/db/backends/utils.py", line 76, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "/home/tim/code/django/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/home/tim/code/spanner-orm/spanner/dbapi/cursor.py", line 94, in execute
    param_types=param_types,
  File "/home/tim/code/spanner-orm/spanner/dbapi/cursor.py", line 175, in __do_execute_non_update
    res = snapshot.execute_sql(sql, params=params, param_types=param_types)
  File "/home/tim/.virtualenvs/django37/lib/python3.7/site-packages/google/cloud/spanner_v1/snapshot.py", line 204, in execute_sql
    fields={key: _make_value_pb(value) for key, value in params.items()}
  File "/home/tim/.virtualenvs/django37/lib/python3.7/site-packages/google/cloud/spanner_v1/snapshot.py", line 204, in <dictcomp>
    fields={key: _make_value_pb(value) for key, value in params.items()}
  File "/home/tim/.virtualenvs/django37/lib/python3.7/site-packages/google/cloud/spanner_v1/_helpers.py", line 91, in _make_value_pb
    raise ValueError("Unknown type: %s" % (value,))
ValueError: Unknown type: 0
```


Discussion:

Resolving patch:
diff --git a/spanner/django/features.py b/spanner/django/features.py
--- a/spanner/django/features.py
+++ b/spanner/django/features.py
@@ -28,6 +28,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
         'aggregation.test_filter_argument.FilteredAggregateTests.test_plain_annotate',
         'aggregation.tests.AggregateTestCase.test_annotate_basic',
         'aggregation.tests.AggregateTestCase.test_annotation',
+        'aggregation.tests.AggregateTestCase.test_filtering',
         'aggregation_regress.tests.AggregationTests.test_more_more',
         'aggregation_regress.tests.AggregationTests.test_more_more_more',
         'aggregation_regress.tests.AggregationTests.test_ticket_11293',
@@ -80,15 +81,9 @@ class DatabaseFeatures(BaseDatabaseFeatures):
         # https://github.com/orijtech/spanner-orm/issues/170
         'model_fields.test_datetimefield.DateTimeFieldTests.test_lookup_date_with_use_tz',
         'model_fields.test_datetimefield.DateTimeFieldTests.test_lookup_date_without_use_tz',
-        # DecimalField lookups crash:
-        # https://github.com/orijtech/spanner-orm/issues/168
-        'aggregation.tests.AggregateTestCase.test_filtering',
+        # filtering on annotation with Decimal value crashes:
+        # https://github.com/orijtech/spanner-orm/issues/210
         'annotations.tests.NonAggregateAnnotationTestCase.test_filter_decimal_annotation',
-        'lookup.test_decimalfield.DecimalFieldLookupTests.test_gt',
-        'lookup.test_decimalfield.DecimalFieldLookupTests.test_gte',
-        'lookup.test_decimalfield.DecimalFieldLookupTests.test_lt',
-        'lookup.test_decimalfield.DecimalFieldLookupTests.test_lte',
-        'model_fields.test_decimalfield.DecimalFieldTests.test_filter_with_strings',
         # using NULL with + crashes: https://github.com/orijtech/spanner-orm/issues/201
         'annotations.tests.NonAggregateAnnotationTestCase.test_combined_annotation_commutative',
         # Spanner loses DecimalField precision due to conversion to float:
diff --git a/spanner/django/lookups.py b/spanner/django/lookups.py
--- a/spanner/django/lookups.py
+++ b/spanner/django/lookups.py
@@ -12,9 +12,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from django.db.models import DecimalField
 from django.db.models.lookups import (
-    Contains, EndsWith, IContains, IEndsWith, IExact, IRegex, IStartsWith,
-    Regex, StartsWith,
+    Contains, EndsWith, Exact, GreaterThan, GreaterThanOrEqual, IContains,
+    IEndsWith, IExact, IRegex, IStartsWith, LessThan, LessThanOrEqual, Regex,
+    StartsWith,
 )
 
 
@@ -78,6 +80,16 @@ def startswith_endswith(self, compiler, connection):
     return rhs_sql % lhs_sql, params
 
 
+def cast_param_to_float(self, compiler, connection):
+    sql, params = self.as_sql(compiler, connection)
+    # Cast to DecimaField lookup values to float because
+    # google.cloud.spanner_v1._helpers._make_value_pb() doesn't serialize
+    # decimal.Decimal.
+    if params and isinstance(self.lhs.output_field, DecimalField):
+        params[0] = float(params[0])
+    return sql, params
+
+
 def register_lookups():
     Contains.as_spanner = contains
     IContains.as_spanner = contains
@@ -88,3 +100,8 @@ def register_lookups():
     IEndsWith.as_spanner = startswith_endswith
     StartsWith.as_spanner = startswith_endswith
     IStartsWith.as_spanner = startswith_endswith
+    Exact.as_spanner = cast_param_to_float
+    GreaterThan.as_spanner = cast_param_to_float
+    GreaterThanOrEqual.as_spanner = cast_param_to_float
+    LessThan.as_spanner = cast_param_to_float
+    LessThanOrEqual.as_spanner = cast_param_to_float


Test patch: