Repository: googleapis/python-spanner-django

Issue:
istartswith, iendswith lookups crash
It seems they were written without testing in https://github.com/orijtech/spanner-orm/pull/87.
```
>>> Group.objects.filter(name__istartswith='test')
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/home/tim/code/django/django/db/models/query.py", line 250, in __repr__
    data = list(self[:REPR_OUTPUT_SIZE + 1])
  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 1087, 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-orm/spanner/django/lookups.py", line 37, in startswith
    rhs_sql = self.get_rhs_op(connection, rhs_sql)
  File "/home/tim/code/django/django/db/models/lookups.py", line 398, in get_rhs_op
    return super().get_rhs_op(connection, rhs)
  File "/home/tim/code/django/django/db/models/lookups.py", line 169, in get_rhs_op
    return connection.operators[self.lookup_name] % rhs
TypeError: not enough arguments for format string
```


Discussion:
I did test them out on a live running app here locally, but indeed we need solid running tests.
I'll send a PR shortly! Thanks for the repro in the filter code, I needed that to have a starting point :)

Resolving patch:
diff --git a/spanner/django/base.py b/spanner/django/base.py
--- a/spanner/django/base.py
+++ b/spanner/django/base.py
@@ -90,8 +90,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
         'lte': '<= %s',
         'startswith': 'STARTS_WITH(%s, %%%%s)',
         'endswith': 'ENDS_WITH(%s, %%%%s)',
-        'istartswith': 'REGEXP_CONTAINS(%s, r"^(?i)%s"',
-        'iendswith': 'REGEXP_CONTAINS(%s, r"(?i)%s$"',
+        'istartswith': 'REGEXP_CONTAINS(%s, %%%%s)',
+        'iendswith': 'REGEXP_CONTAINS(%s, %%%%s)',
     }
 
     Database = Database
diff --git a/spanner/django/lookups.py b/spanner/django/lookups.py
--- a/spanner/django/lookups.py
+++ b/spanner/django/lookups.py
@@ -17,30 +17,29 @@
 )
 
 
-def endswith(self, compiler, connection):
+def startswith_endswith(self, compiler, connection):
+    """startswith, endswith, istartswith, and iendswith lookups."""
     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)
-    # Chop leading '%' from param since this isn't a LIKE query.
-    params[0] = params[0][1:]
-    # rhs_sql is 'STARTS_WITH(%s, %%s)' and lhs_sql is the column name.
-    return rhs_sql % lhs_sql, params
-
-
-def startswith(self, compiler, connection):
-    # Same logic as endswith but chop trailing (rather than leading) '%' from
-    # param.
-    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)
-    params[0] = params[0][:-1]
+    # 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:
+        params[0] = params[0][1:]
+    else:
+        params[0] = params[0][:-1]
+    # Construct the regex for istartswith or iendswith.
+    if self.lookup_name.startswith('i'):
+        regex = '^(?i)%s' if self.lookup_name == 'istartswith' else '(?i)%s$'
+        params[0] = regex % params[0]
+    # rhs_sql is e.g. 'STARTS_WITH(%s, %%s)' or REGEXP_CONTAINS(%s, %%s), and
+    # lhs_sql is the column name.
     return rhs_sql % lhs_sql, params
 
 
 def register_lookups():
-    EndsWith.as_spanner = endswith
-    IEndsWith.as_spanner = endswith
-    StartsWith.as_spanner = startswith
-    IStartsWith.as_spanner = startswith
+    EndsWith.as_spanner = startswith_endswith
+    IEndsWith.as_spanner = startswith_endswith
+    StartsWith.as_spanner = startswith_endswith
+    IStartsWith.as_spanner = startswith_endswith


Test patch: