Repository: googleapis/python-spanner-django

Issue:
django: contains lookup crashes with bilateral transform
```
======================================================================
ERROR: test_bilateral_upper (custom_lookups.tests.BilateralTransformTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tim/code/django/tests/custom_lookups/tests.py", line 329, in test_bilateral_upper
    ["<Author: Foo>"], ordered=False)
  File "/home/tim/code/django/django/test/testcases.py", line 1047, in assertQuerysetEqual
    items = map(transform, qs)
  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 1127, in execute_sql
    sql, params = self.as_sql()
  File "/home/tim/code/django/django/db/models/sql/compiler.py", line 489, in as_sql
    where, w_params = self.compile(self.where) if self.where is not None else ("", [])
  File "/home/tim/code/django/django/db/models/sql/compiler.py", line 405, in compile
    sql, params = node.as_sql(self, self.connection)
  File "/home/tim/code/django/django/db/models/sql/where.py", line 81, in as_sql
    sql, params = compiler.compile(child)
  File "/home/tim/code/django/django/db/models/sql/compiler.py", line 403, in compile
    sql, params = vendor_impl(self, self.connection)
  File "/home/tim/code/spanner-django/django_spanner/lookups.py", line 30, in contains
    return rhs_sql % lhs_sql, params
TypeError: not all arguments converted during string formatting
```


Discussion:

Resolving patch:
diff --git a/django_spanner/features.py b/django_spanner/features.py
--- a/django_spanner/features.py
+++ b/django_spanner/features.py
@@ -157,9 +157,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
         'model_fields.test_decimalfield.DecimalFieldTests.test_roundtrip_with_trailing_zeros',
         # No CHECK constraints in Spanner.
         'model_fields.test_integerfield.PositiveIntegerFieldTests.test_negative_values',
-        # contains lookup crashes with bilateral transform:
-        # https://github.com/googleapis/python-spanner-django/issues/419
-        'custom_lookups.tests.BilateralTransformTests.test_bilateral_upper',
         # Spanner doesn't support the variance the standard deviation database
         # functions:
         'aggregation.test_filter_argument.FilteredAggregateTests.test_filtered_numerical_aggregates',
diff --git a/django_spanner/lookups.py b/django_spanner/lookups.py
--- a/django_spanner/lookups.py
+++ b/django_spanner/lookups.py
@@ -17,9 +17,9 @@ def contains(self, compiler, connection):
     lhs_sql, params = self.process_lhs(compiler, connection)
     rhs_sql, rhs_params = self.process_rhs(compiler, connection)
     params.extend(rhs_params)
-    rhs_sql = self.get_rhs_op(connection, rhs_sql)
     is_icontains = self.lookup_name.startswith('i')
     if self.rhs_is_direct_value() and params and not self.bilateral_transforms:
+        rhs_sql = self.get_rhs_op(connection, rhs_sql)
         # Chop the leading and trailing percent signs that Django adds to the
         # param since this isn't a LIKE query as Django expects.
         params[0] = params[0][1:-1]
@@ -29,12 +29,11 @@ def contains(self, compiler, connection):
         # rhs_sql is REGEXP_CONTAINS(%s, %%s), and lhs_sql is the column name.
         return rhs_sql % lhs_sql, params
     else:
-        # rhs is the expression/column to use as the base of the regular
+        # rhs_sql is the expression/column to use as the base of the regular
         # expression.
-        rhs = compiler.compile(self.rhs)[0]
         if is_icontains:
-            rhs = "CONCAT('(?i)', " + rhs + ")"
-        return 'REGEXP_CONTAINS(%s, %s)' % (lhs_sql, connection.pattern_esc.format(rhs)), params
+            rhs_sql = "CONCAT('(?i)', " + rhs_sql + ")"
+        return 'REGEXP_CONTAINS(%s, %s)' % (lhs_sql, connection.pattern_esc.format(rhs_sql)), params
 
 
 def iexact(self, compiler, connection):
@@ -59,9 +58,9 @@ def regex(self, compiler, connection):
     lhs_sql, params = self.process_lhs(compiler, connection)
     rhs_sql, rhs_params = self.process_rhs(compiler, connection)
     params.extend(rhs_params)
-    rhs_sql = self.get_rhs_op(connection, rhs_sql)
     is_iregex = self.lookup_name.startswith('i')
     if self.rhs_is_direct_value() and params and not self.bilateral_transforms:
+        rhs_sql = self.get_rhs_op(connection, rhs_sql)
         if is_iregex:
             params[0] = '(?i)%s' % params[0]
         else:
@@ -69,12 +68,11 @@ def regex(self, compiler, connection):
         # rhs_sql is REGEXP_CONTAINS(%s, %%s), and lhs_sql is the column name.
         return rhs_sql % lhs_sql, params
     else:
-        # rhs is the expression/column to use as the base of the regular
+        # rhs_sql is the expression/column to use as the base of the regular
         # expression.
-        rhs = compiler.compile(self.rhs)[0]
         if is_iregex:
-            rhs = "CONCAT('(?i)', " + rhs + ")"
-        return 'REGEXP_CONTAINS(%s, %s)' % (lhs_sql, rhs), params
+            rhs_sql = "CONCAT('(?i)', " + rhs_sql + ")"
+        return 'REGEXP_CONTAINS(%s, %s)' % (lhs_sql, rhs_sql), params
 
 
 def startswith_endswith(self, compiler, connection):
@@ -82,13 +80,13 @@ def startswith_endswith(self, compiler, connection):
     lhs_sql, params = self.process_lhs(compiler, connection)
     rhs_sql, rhs_params = self.process_rhs(compiler, connection)
     params.extend(rhs_params)
-    rhs_sql = self.get_rhs_op(connection, rhs_sql)
     is_startswith = 'startswith' in self.lookup_name
     is_endswith = 'endswith' in self.lookup_name
     is_insensitive = self.lookup_name.startswith('i')
     # 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 self.rhs_is_direct_value() and params and not self.bilateral_transforms:
+        rhs_sql = self.get_rhs_op(connection, rhs_sql)
         if is_endswith:
             params[0] = str(params[0][1:]) + '$'
         else:
@@ -99,14 +97,14 @@ def startswith_endswith(self, compiler, connection):
         # rhs_sql is REGEXP_CONTAINS(%s, %%s), and lhs_sql is the column name.
         return rhs_sql % lhs_sql, params
     else:
-        # rhs is the expression/column to use as the base of the regular
+        # rhs_sql is the expression/column to use as the base of the regular
         # expression.
         sql = "CONCAT('"
         if is_startswith:
             sql += '^'
         if is_insensitive:
             sql += '(?i)'
-        sql += "', " + compiler.compile(self.rhs)[0]
+        sql += "', " + rhs_sql
         if is_endswith:
             sql += ", '$'"
         sql += ")"


Test patch: