Repository: googleapis/python-spanner-django

Issue:
Make iexact and icontains lookups case insenstive
I'd think they could use `REGEXP_CONTAINS()` with `(?i)` to be case insensitive.


Discussion:

Resolving patch:
diff --git a/spanner/django/base.py b/spanner/django/base.py
--- a/spanner/django/base.py
+++ b/spanner/django/base.py
@@ -79,11 +79,9 @@ class DatabaseWrapper(BaseDatabaseWrapper):
 
     operators = {
         'exact': '= %s',
-        # Spanner doesn't have a case insensitive matching.
-        'iexact': '= %s',
+        'iexact': 'REGEXP_CONTAINS(%s, %%%%s)',
         'contains':  'LIKE %s',
-        # Spanner doesn't have a case insensitive LIKE.
-        'icontains': 'LIKE %s',
+        'icontains': 'REGEXP_CONTAINS(%s, %%%%s)',
         'gt': '> %s',
         'gte': '>= %s',
         'lt': '< %s',
diff --git a/spanner/django/lookups.py b/spanner/django/lookups.py
--- a/spanner/django/lookups.py
+++ b/spanner/django/lookups.py
@@ -12,11 +12,36 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import re
+
 from django.db.models.lookups import (
-    EndsWith, IEndsWith, IStartsWith, StartsWith,
+    EndsWith, IContains, IEndsWith, IExact, IStartsWith, StartsWith,
 )
 
 
+def icontains(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)
+    # 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] = '(?i)%s' % re.escape(params[0][1:-1])
+    # rhs_sql is REGEXP_CONTAINS(%s, %%s), and lhs_sql is the column name.
+    return rhs_sql % lhs_sql, params
+
+
+def iexact(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)
+    # Wrap the parameter in ^ and $ to restrict the regex to an exact match.
+    params[0] = '^(?i)%s$' % re.escape(params[0])
+    # rhs_sql is REGEXP_CONTAINS(%s, %%s), and lhs_sql is the column name.
+    return rhs_sql % lhs_sql, params
+
+
 def startswith_endswith(self, compiler, connection):
     """startswith, endswith, istartswith, and iendswith lookups."""
     lhs_sql, params = self.process_lhs(compiler, connection)
@@ -32,13 +57,15 @@ def startswith_endswith(self, compiler, connection):
     # 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]
+        params[0] = regex % re.escape(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():
+    IContains.as_spanner = icontains
+    IExact.as_spanner = iexact
     EndsWith.as_spanner = startswith_endswith
     IEndsWith.as_spanner = startswith_endswith
     StartsWith.as_spanner = startswith_endswith


Test patch: