Repository: googleapis/python-spanner-django

Issue:
django-test-suite: lookup.tests.LookupTests.test_lookup_int_as_str
```
======================================================================
ERROR: test_lookup_int_as_str (lookup.tests.LookupTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tim/.virtualenvs/django37/lib/python3.7/site-packages/google/api_core/grpc_helpers.py", line 79, in next
    return six.next(self._wrapped)
  File "/home/tim/.virtualenvs/django37/lib/python3.7/site-packages/grpc/_channel.py", line 392, in __next__
    return self._next()
  File "/home/tim/.virtualenvs/django37/lib/python3.7/site-packages/grpc/_channel.py", line 561, in _next
    raise self
grpc._channel._Rendezvous: <_Rendezvous of RPC that terminated with:
	status = StatusCode.INVALID_ARGUMENT
	details = "No matching signature for function REGEXP_CONTAINS for argument types: INT64, INT64. Supported signatures: REGEXP_CONTAINS(STRING, STRING); REGEXP_CONTAINS(BYTES, BYTES) [at 1:149]\n...lookup_article.slug FROM lookup_article WHERE REGEXP_CONTAINS(lookup_artic...\n                                                 ^"
	debug_error_string = "{"created":"@1576095084.721434711","description":"Error received from peer ipv4:172.217.9.234:443","file":"src/core/lib/surface/call.cc","file_line":1055,"grpc_message":"No matching signature for function REGEXP_CONTAINS for argument types: INT64, INT64. Supported signatures: REGEXP_CONTAINS(STRING, STRING); REGEXP_CONTAINS(BYTES, BYTES) [at 1:149]\n...lookup_article.slug FROM lookup_article WHERE REGEXP_CONTAINS(lookup_artic...\n                                                 ^","grpc_status":3}"
```


Discussion:
I can confirm that my sunset PR https://github.com/orijtech/spanner-orm/pull/141 fixes this issue
with the diff
```diff
diff --git a/spanner/django/lookups.py b/spanner/django/lookups.py
index a1669b3..6941454 100644
--- a/spanner/django/lookups.py
+++ b/spanner/django/lookups.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from django.db.models import CharField
 from django.db.models.lookups import (
     Contains, EndsWith, IContains, IEndsWith, IExact, IRegex, IStartsWith,
     Regex, StartsWith,
@@ -22,6 +23,8 @@ def contains(self, compiler, connection):
     """contains and icontains"""
     lhs_sql, params = self.process_lhs(compiler, connection)
     rhs_sql, rhs_params = self.process_rhs(compiler, connection)
+    if lhs_requires_cast_to_str(self.lhs.output_field):
+        lhs_sql = 'CAST(' + lhs_sql + ' AS STRING)'
     params.extend(rhs_params)
     rhs_sql = self.get_rhs_op(connection, rhs_sql)
     # Chop the leading and trailing percent signs that Django adds to the
@@ -37,6 +40,8 @@ def contains(self, compiler, connection):
 def iexact(self, compiler, connection):
     lhs_sql, params = self.process_lhs(compiler, connection)
     rhs_sql, rhs_params = self.process_rhs(compiler, connection)
+    if lhs_requires_cast_to_str(self.lhs.output_field):
+        lhs_sql = 'CAST(' + lhs_sql + ' AS STRING)'
     params.extend(rhs_params)
     rhs_sql = self.get_rhs_op(connection, rhs_sql)
     # Wrap the parameter in ^ and $ to restrict the regex to an exact match.
@@ -65,6 +70,8 @@ def startswith_endswith(self, compiler, connection):
     rhs_sql, rhs_params = self.process_rhs(compiler, connection)
     params.extend(rhs_params)
     rhs_sql = self.get_rhs_op(connection, rhs_sql)
+    if lhs_requires_cast_to_str(self.lhs.output_field):
+        lhs_sql = 'CAST(' + lhs_sql + ' AS STRING)'
     # Chop the leading (endswith) or trailing (startswith) percent sign that
     # Django adds to the param since this isn't a LIKE query as Django expects.
     if 'endswith' in self.lookup_name:
@@ -78,6 +85,17 @@ def startswith_endswith(self, compiler, connection):
     return rhs_sql % lhs_sql, params
 
 
+def lhs_requires_cast_to_str(lhs_field):
+    """
+    Report whether the Left Hand Side field will need a CAST <field> AS STRING
+    since string lookup operations are being performed on it.
+    See:
+    * Issue #138
+    * https://cloud.google.com/spanner/docs/functions-and-operators#casting
+    """
+    return not isinstance(lhs_field, CharField)
+
+
 def register_lookups():
     Contains.as_spanner = contains
     IContains.as_spanner = contains
```
I had mailed PR https://github.com/orijtech/django-spanner/pull/141/ but with PR feedback and the evolution of the solution, I deferred to you @timgraham as per https://github.com/orijtech/django-spanner/pull/141/#issuecomment-563491501. All yours, please take it over.

Resolving patch:
diff --git a/django_spanner/features.py b/django_spanner/features.py
--- a/django_spanner/features.py
+++ b/django_spanner/features.py
@@ -217,9 +217,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
         'one_to_one.tests.OneToOneTests.test_multiple_o2o',
         # No CHECK constraints in Spanner.
         'model_fields.test_integerfield.PositiveIntegerFieldTests.test_negative_values',
-        # No matching signature for function REGEXP_CONTAINS for argument
-        # types: INT64, INT64: https://github.com/orijtech/spanner-orm/issues/177
-        'lookup.tests.LookupTests.test_lookup_int_as_str',
         # 'DatabaseWrapper' object has no attribute 'pattern_ops'
         # https://github.com/orijtech/spanner-orm/issues/178
         'expressions.tests.BasicExpressionsTests.test_ticket_16731_startswith_lookup',
diff --git a/django_spanner/operations.py b/django_spanner/operations.py
--- a/django_spanner/operations.py
+++ b/django_spanner/operations.py
@@ -229,7 +229,7 @@ def combine_duration_expression(self, connector, sub_expressions):
     def lookup_cast(self, lookup_type, internal_type=None):
         # Cast text lookups to string to allow things like filter(x__contains=4)
         if lookup_type in ('contains', 'icontains', 'startswith', 'istartswith',
-                           'endswith', 'iendswith', 'regex', 'iregex'):
+                           'endswith', 'iendswith', 'regex', 'iregex', 'iexact'):
             return 'CAST(%s AS STRING)'
         return '%s'
 


Test patch: