Repository: googleapis/python-spanner-django

Issue:
django: (i)contains, (i)startswith, and (i)endswith lookups with database function crash
The solution from #178 needs to be extended to work with database functions.
```
======================================================================
ERROR: test_pattern_lookups_with_substr (lookup.tests.LookupTests) (lookup='startswith')
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tim/code/django/tests/lookup/tests.py", line 919, in test_pattern_lookups_with_substr
    self.assertCountEqual(authors, result)
  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 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 87, in startswith_endswith
    params[0] = '^' + str(params[0][:-1])
TypeError: 'int' object is not subscriptable
```


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
@@ -136,9 +136,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',
-        # (i)contains, (i)startswith, and (i)endswith lookups with database
-        # function crash: https://github.com/googleapis/python-spanner-django/issues/409
-        'lookup.tests.LookupTests.test_pattern_lookups_with_substr',
         # 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
@@ -19,7 +19,7 @@ def contains(self, compiler, connection):
     params.extend(rhs_params)
     rhs_sql = self.get_rhs_op(connection, rhs_sql)
     is_icontains = self.lookup_name.startswith('i')
-    if params:
+    if self.rhs_is_direct_value() and params and not self.bilateral_transforms:
         # 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,12 @@ 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:
-        # If no params, then rhs is the expression/column to use as the base of
-        # the regular expression.
-        rhs, _ = self.rhs.as_sql(compiler, connection)
+        # rhs 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)), []
+        return 'REGEXP_CONTAINS(%s, %s)' % (lhs_sql, connection.pattern_esc.format(rhs)), params
 
 
 def iexact(self, compiler, connection):
@@ -43,11 +43,11 @@ def iexact(self, compiler, connection):
     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.
-    if params:
+    if self.rhs_is_direct_value() and params and not self.bilateral_transforms:
         params[0] = '^(?i)%s$' % params[0]
     else:
-        # If no params, then lhs_sql is the expression/column to use as the
-        # regular expression. Use concat to make the value case-insensitive.
+        # lhs_sql is the expression/column to use as the regular expression.
+        # Use concat to make the value case-insensitive.
         lhs_sql = "CONCAT('^(?i)', " + lhs_sql + ", '$')"
         rhs_sql = rhs_sql.replace('%%s', '%s')
     # rhs_sql is REGEXP_CONTAINS(%s, %%s), and lhs_sql is the column name.
@@ -79,7 +79,7 @@ def startswith_endswith(self, compiler, connection):
     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 params:
+    if self.rhs_is_direct_value() and params and not self.bilateral_transforms:
         if is_endswith:
             params[0] = str(params[0][1:]) + '$'
         else:
@@ -90,18 +90,18 @@ 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:
-        # If no params, then rhs is the expression/column to use as the base
-        # of the regular expression.
+        # rhs 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 += "', " + self.rhs.as_sql(compiler, connection)[0]
+        sql += "', " + compiler.compile(self.rhs)[0]
         if is_endswith:
             sql += ", '$'"
         sql += ")"
-        return 'REGEXP_CONTAINS(%s, %s)' % (lhs_sql, connection.pattern_esc.format(sql)), []
+        return 'REGEXP_CONTAINS(%s, %s)' % (lhs_sql, connection.pattern_esc.format(sql)), params
 
 
 def cast_param_to_float(self, compiler, connection):


Test patch: