Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #1465: added support for regex lookups. Thanks, Tom Tobin.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@5555 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 24512a74befc6282a1d299cab452ee9463cc2baa 1 parent 7dc8b1a
Jacob Kaplan-Moss authored
2  django/db/backends/mysql/base.py
@@ -247,6 +247,8 @@ def get_sql_sequence_reset(style, model_list):
247 247
     'iexact': 'LIKE %s',
248 248
     'contains': 'LIKE BINARY %s',
249 249
     'icontains': 'LIKE %s',
  250
+    'regex': 'REGEXP BINARY %s',
  251
+    'iregex': 'REGEXP %s',
250 252
     'gt': '> %s',
251 253
     'gte': '>= %s',
252 254
     'lt': '< %s',
2  django/db/backends/mysql_old/base.py
@@ -248,6 +248,8 @@ def get_sql_sequence_reset(style, model_list):
248 248
     'iexact': 'LIKE %s',
249 249
     'contains': 'LIKE BINARY %s',
250 250
     'icontains': 'LIKE %s',
  251
+    'regex': 'REGEXP BINARY %s',
  252
+    'iregex': 'REGEXP %s',
251 253
     'gt': '> %s',
252 254
     'gte': '>= %s',
253 255
     'lt': '< %s',
2  django/db/backends/postgresql/base.py
@@ -280,6 +280,8 @@ def get_sql_sequence_reset(style, model_list):
280 280
     'iexact': 'ILIKE %s',
281 281
     'contains': 'LIKE %s',
282 282
     'icontains': 'ILIKE %s',
  283
+    'regex': '~ %s',
  284
+    'iregex': '~* %s',
283 285
     'gt': '> %s',
284 286
     'gte': '>= %s',
285 287
     'lt': '< %s',
2  django/db/backends/postgresql_psycopg2/base.py
@@ -225,6 +225,8 @@ def get_sql_sequence_reset(style, model_list):
225 225
     'iexact': 'ILIKE %s',
226 226
     'contains': 'LIKE %s',
227 227
     'icontains': 'ILIKE %s',
  228
+    'regex': '~ %s',
  229
+    'iregex': '~* %s',
228 230
     'gt': '> %s',
229 231
     'gte': '>= %s',
230 232
     'lt': '< %s',
12  django/db/backends/sqlite3/base.py
@@ -64,9 +64,10 @@ def cursor(self):
64 64
             }
65 65
             kwargs.update(self.options)
66 66
             self.connection = Database.connect(**kwargs)
67  
-            # Register extract and date_trunc functions.
  67
+            # Register extract, date_trunc, and regexp functions.
68 68
             self.connection.create_function("django_extract", 2, _sqlite_extract)
69 69
             self.connection.create_function("django_date_trunc", 2, _sqlite_date_trunc)
  70
+            self.connection.create_function("regexp", 2, _sqlite_regexp)
70 71
         cursor = self.connection.cursor(factory=SQLiteCursorWrapper)
71 72
         cursor.row_factory = utf8rowFactory
72 73
         if settings.DEBUG:
@@ -214,6 +215,13 @@ def _sqlite_date_trunc(lookup_type, dt):
214 215
     elif lookup_type == 'day':
215 216
         return "%i-%02i-%02i 00:00:00" % (dt.year, dt.month, dt.day)
216 217
 
  218
+def _sqlite_regexp(re_pattern, re_string):
  219
+    import re
  220
+    try:
  221
+        return bool(re.search(re_pattern, re_string))
  222
+    except:
  223
+        return False
  224
+
217 225
 # SQLite requires LIKE statements to include an ESCAPE clause if the value
218 226
 # being escaped has a percent or underscore in it.
219 227
 # See http://www.sqlite.org/lang_expr.html for an explanation.
@@ -222,6 +230,8 @@ def _sqlite_date_trunc(lookup_type, dt):
222 230
     'iexact': "LIKE %s ESCAPE '\\'",
223 231
     'contains': "LIKE %s ESCAPE '\\'",
224 232
     'icontains': "LIKE %s ESCAPE '\\'",
  233
+    'regex': 'REGEXP %s',
  234
+    'iregex': "REGEXP '(?i)' || %s",
225 235
     'gt': '> %s',
226 236
     'gte': '>= %s',
227 237
     'lt': '< %s',
2  django/db/models/fields/__init__.py
@@ -174,7 +174,7 @@ def get_db_prep_save(self, value):
174 174
 
175 175
     def get_db_prep_lookup(self, lookup_type, value):
176 176
         "Returns field's value prepared for database lookup."
177  
-        if lookup_type in ('exact', 'gt', 'gte', 'lt', 'lte', 'month', 'day', 'search'):
  177
+        if lookup_type in ('exact', 'regex', 'iregex', 'gt', 'gte', 'lt', 'lte', 'month', 'day', 'search'):
178 178
             return [value]
179 179
         elif lookup_type in ('range', 'in'):
180 180
             return value
11  django/db/models/query.py
... ...
@@ -1,3 +1,4 @@
  1
+from django.conf import settings
1 2
 from django.db import backend, connection, transaction
2 3
 from django.db.models.fields import DateField, FieldDoesNotExist
3 4
 from django.db.models import signals, loading
@@ -22,6 +23,7 @@
22 23
     'gt', 'gte', 'lt', 'lte', 'in',
23 24
     'startswith', 'istartswith', 'endswith', 'iendswith',
24 25
     'range', 'year', 'month', 'day', 'isnull', 'search',
  26
+    'regex', 'iregex',
25 27
 )
26 28
 
27 29
 # Size of each "chunk" for get_iterator calls.
@@ -797,6 +799,15 @@ def get_where_clause(lookup_type, table_prefix, field_name, value):
797 799
         return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or ''))
798 800
     elif lookup_type == 'search':
799 801
         return backend.get_fulltext_search_sql(table_prefix + field_name)
  802
+    elif lookup_type in ('regex', 'iregex'):
  803
+        if settings.DATABASE_ENGINE == 'oracle':
  804
+            if lookup_type == 'regex':
  805
+                match_option = 'c'
  806
+            else:
  807
+                match_option = 'i'
  808
+            return "REGEXP_LIKE(%s%s, %s, '%s')" % (table_prefix, field_name, cast_sql, match_option)
  809
+        else:
  810
+            raise NotImplementedError
800 811
     raise TypeError, "Got invalid lookup_type: %s" % repr(lookup_type)
801 812
 
802 813
 def get_cached_row(klass, row, index_start, max_depth=0, cur_depth=0):
42  docs/db-api.txt
@@ -1173,6 +1173,48 @@ like ``contains`` but is significantly faster due to full-text indexing.
1173 1173
 Note this is only available in MySQL and requires direct manipulation of the
1174 1174
 database to add the full-text index.
1175 1175
 
  1176
+regex
  1177
+~~~~~
  1178
+
  1179
+Case-sensitive regular expression match.
  1180
+
  1181
+The regular expression syntax is that of the database backend in use; for the
  1182
+``sqlite`` backend, the syntax is that of Python's ``re`` module.
  1183
+
  1184
+Example::
  1185
+
  1186
+    Entry.objects.get(title__regex=r'^(An?|The) +')
  1187
+
  1188
+SQL equivalents::
  1189
+
  1190
+    SELECT ... WHERE title REGEXP BINARY '^(An?|The) +'; -- MySQL
  1191
+
  1192
+    SELECT ... WHERE title ~ '^(An?|The) +'; -- PostgreSQL
  1193
+
  1194
+    SELECT ... WHERE title REGEXP '^(An?|The) +'; -- sqlite
  1195
+
  1196
+Using raw strings for passing in the regular expression syntax is recommended.
  1197
+
  1198
+Regular expression matching is not supported on the ``ado_mssql`` and
  1199
+``oracle`` backends; these will raise a ``NotImplementedError``.
  1200
+
  1201
+iregex
  1202
+~~~~~~
  1203
+
  1204
+Case-insensitive regular expression match.
  1205
+
  1206
+Example::
  1207
+
  1208
+    Entry.objects.get(title__iregex=r'^(an?|the) +')
  1209
+
  1210
+SQL equivalents::
  1211
+
  1212
+    SELECT ... WHERE title REGEXP '^(an?|the) +'; -- MySQL
  1213
+
  1214
+    SELECT ... WHERE title ~* '^(an?|the) +'; -- PostgreSQL
  1215
+
  1216
+    SELECT ... WHERE title REGEXP '(?i)^(an?|the) +'; -- sqlite
  1217
+
1176 1218
 Default lookups are exact
1177 1219
 -------------------------
1178 1220
 
94  tests/modeltests/lookup/models.py
@@ -251,4 +251,98 @@ def __str__(self):
251 251
     ...
252 252
 TypeError: Cannot resolve keyword 'headline__starts' into field. Choices are: id, headline, pub_date
253 253
 
  254
+# Create some articles with a bit more interesting headlines for testing field lookups:
  255
+>>> now = datetime.now()
  256
+>>> for a in Article.objects.all():
  257
+...     a.delete()
  258
+>>> a1 = Article(pub_date=now, headline='f')
  259
+>>> a1.save()
  260
+>>> a2 = Article(pub_date=now, headline='fo')
  261
+>>> a2.save()
  262
+>>> a3 = Article(pub_date=now, headline='foo')
  263
+>>> a3.save()
  264
+>>> a4 = Article(pub_date=now, headline='fooo')
  265
+>>> a4.save()
  266
+>>> a5 = Article(pub_date=now, headline='Foo')
  267
+>>> a5.save()
  268
+
  269
+# zero-or-more
  270
+>>> Article.objects.filter(headline__regex=r'fo*')
  271
+[<Article: f>, <Article: fo>, <Article: foo>, <Article: fooo>]
  272
+>>> Article.objects.filter(headline__iregex=r'fo*')
  273
+[<Article: Foo>, <Article: f>, <Article: fo>, <Article: foo>, <Article: fooo>]
  274
+
  275
+# one-or-more
  276
+>>> Article.objects.filter(headline__regex=r'fo+')
  277
+[<Article: fo>, <Article: foo>, <Article: fooo>]
  278
+
  279
+# wildcard
  280
+>>> Article.objects.filter(headline__regex=r'fooo?')
  281
+[<Article: foo>, <Article: fooo>]
  282
+
  283
+# and some more:
  284
+>>> a6 = Article(pub_date=now, headline='bar')
  285
+>>> a6.save()
  286
+>>> a7 = Article(pub_date=now, headline='Bar')
  287
+>>> a7.save()
  288
+>>> a8 = Article(pub_date=now, headline='baz')
  289
+>>> a8.save()
  290
+>>> a9 = Article(pub_date=now, headline='baZ')
  291
+>>> a9.save()
  292
+
  293
+# leading anchor
  294
+>>> Article.objects.filter(headline__regex=r'^b')
  295
+[<Article: baZ>, <Article: bar>, <Article: baz>]
  296
+>>> Article.objects.filter(headline__iregex=r'^b')
  297
+[<Article: Bar>, <Article: baZ>, <Article: bar>, <Article: baz>]
  298
+
  299
+# trailing anchor
  300
+>>> Article.objects.filter(headline__regex=r'z$')
  301
+[<Article: baz>]
  302
+>>> Article.objects.filter(headline__iregex=r'z$')
  303
+[<Article: baZ>, <Article: baz>]
  304
+
  305
+# character sets
  306
+>>> Article.objects.filter(headline__regex=r'ba[rz]')
  307
+[<Article: bar>, <Article: baz>]
  308
+>>> Article.objects.filter(headline__regex=r'ba[RZ]')
  309
+[<Article: baZ>]
  310
+>>> Article.objects.filter(headline__iregex=r'ba[RZ]')
  311
+[<Article: Bar>, <Article: baZ>, <Article: bar>, <Article: baz>]
  312
+
  313
+# and yet more:
  314
+>>> a10 = Article(pub_date=now, headline='foobar')
  315
+>>> a10.save()
  316
+>>> a11 = Article(pub_date=now, headline='foobaz')
  317
+>>> a11.save()
  318
+>>> a12 = Article(pub_date=now, headline='FooBarBaz')
  319
+>>> a12.save()
  320
+>>> a13 = Article(pub_date=now, headline='foobarbaz')
  321
+>>> a13.save()
  322
+>>> a14 = Article(pub_date=now, headline='zoocarfaz')
  323
+>>> a14.save()
  324
+>>> a15 = Article(pub_date=now, headline='barfoobaz')
  325
+>>> a15.save()
  326
+>>> a16 = Article(pub_date=now, headline='BAZBARFOO')
  327
+>>> a16.save()
  328
+
  329
+# alternation
  330
+>>> Article.objects.filter(headline__regex=r'foo(bar|baz)')
  331
+[<Article: barfoobaz>, <Article: foobar>, <Article: foobarbaz>, <Article: foobaz>]
  332
+>>> Article.objects.filter(headline__iregex=r'foo(bar|baz)')
  333
+[<Article: FooBarBaz>, <Article: barfoobaz>, <Article: foobar>, <Article: foobarbaz>, <Article: foobaz>]
  334
+>>> Article.objects.filter(headline__regex=r'^foo(bar|baz)')
  335
+[<Article: foobar>, <Article: foobarbaz>, <Article: foobaz>]
  336
+
  337
+# greedy matching
  338
+>>> Article.objects.filter(headline__regex=r'f.*z')
  339
+[<Article: barfoobaz>, <Article: foobarbaz>, <Article: foobaz>, <Article: zoocarfaz>]
  340
+>>> Article.objects.filter(headline__iregex=r'f.*z')
  341
+[<Article: FooBarBaz>, <Article: barfoobaz>, <Article: foobarbaz>, <Article: foobaz>, <Article: zoocarfaz>]
  342
+
  343
+# grouping and backreferences
  344
+>>> Article.objects.filter(headline__regex=r'b(.).*b\1')
  345
+[<Article: barfoobaz>, <Article: foobarbaz>]
  346
+>>> Article.objects.filter(headline__iregex=r'b(.).*b\1')
  347
+[<Article: BAZBARFOO>, <Article: FooBarBaz>, <Article: barfoobaz>, <Article: foobarbaz>]
254 348
 """}
3  tests/regressiontests/templates/tests.py
@@ -219,6 +219,9 @@ def test_templates(self):
219 219
             # value will be converted to a bytestring.
220 220
             'filter-syntax18': (r'{{ var }}', {'var': UnicodeInStrClass()}, '\xc5\xa0\xc4\x90\xc4\x86\xc5\xbd\xc4\x87\xc5\xbe\xc5\xa1\xc4\x91'),
221 221
 
  222
+            # Numbers as filter arguments should work
  223
+            'filter-syntax19': ('{{ var|truncatewords:1 }}', {"var": "hello world"}, "hello ..."),
  224
+
222 225
             ### COMMENT SYNTAX ########################################################
223 226
             'comment-syntax01': ("{# this is hidden #}hello", {}, "hello"),
224 227
             'comment-syntax02': ("{# this is hidden #}hello{# foo #}", {}, "hello"),

0 notes on commit 24512a7

Please sign in to comment.
Something went wrong with that request. Please try again.