summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMariusz Felisiak <felisiak.mariusz@gmail.com>2019-02-21 10:52:51 +0100
committerGitHub <noreply@github.com>2019-02-21 10:52:51 +0100
commit9ff18c08c32cf54d3c3a7a9e459d44711adba30f (patch)
treed244a2f0133ddeabbc879b3dc65b31a4b0603d03
parent21ff23bfeb4014bceaa3df27677fb68409c0634d (diff)
Refs #28643 -- Added MD5 database function.
Thanks Tim Graham, Nick Pope and Simon Charette for reviews.
-rw-r--r--django/db/backends/sqlite3/base.py2
-rw-r--r--django/db/models/functions/__init__.py9
-rw-r--r--django/db/models/functions/text.py16
-rw-r--r--docs/ref/models/database-functions.txt20
-rw-r--r--docs/releases/3.0.txt2
-rw-r--r--tests/db_functions/text/test_md5.py41
6 files changed, 85 insertions, 5 deletions
diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py
index f7207a2b95..9f30b75e6a 100644
--- a/django/db/backends/sqlite3/base.py
+++ b/django/db/backends/sqlite3/base.py
@@ -4,6 +4,7 @@ SQLite backend for the sqlite3 module in the standard library.
import datetime
import decimal
import functools
+import hashlib
import math
import operator
import re
@@ -217,6 +218,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
conn.create_function('LN', 1, none_guard(math.log))
conn.create_function('LOG', 2, none_guard(lambda x, y: math.log(y, x)))
conn.create_function('LPAD', 3, _sqlite_lpad)
+ conn.create_function('MD5', 1, none_guard(lambda x: hashlib.md5(x.encode()).hexdigest()))
conn.create_function('MOD', 2, none_guard(math.fmod))
conn.create_function('PI', 0, lambda: math.pi)
conn.create_function('POWER', 2, none_guard(operator.pow))
diff --git a/django/db/models/functions/__init__.py b/django/db/models/functions/__init__.py
index ed863b92af..88bc43431a 100644
--- a/django/db/models/functions/__init__.py
+++ b/django/db/models/functions/__init__.py
@@ -10,8 +10,9 @@ from .math import (
Mod, Pi, Power, Radians, Round, Sin, Sqrt, Tan,
)
from .text import (
- Chr, Concat, ConcatPair, Left, Length, Lower, LPad, LTrim, Ord, Repeat,
- Replace, Reverse, Right, RPad, RTrim, StrIndex, Substr, Trim, Upper,
+ MD5, Chr, Concat, ConcatPair, Left, Length, Lower, LPad, LTrim, Ord,
+ Repeat, Replace, Reverse, Right, RPad, RTrim, StrIndex, Substr, Trim,
+ Upper,
)
from .window import (
CumeDist, DenseRank, FirstValue, Lag, LastValue, Lead, NthValue, Ntile,
@@ -33,8 +34,8 @@ __all__ = [
'Exp', 'Floor', 'Ln', 'Log', 'Mod', 'Pi', 'Power', 'Radians', 'Round',
'Sin', 'Sqrt', 'Tan',
# text
- 'Chr', 'Concat', 'ConcatPair', 'Left', 'Length', 'Lower', 'LPad', 'LTrim',
- 'Ord', 'Repeat', 'Replace', 'Reverse', 'Right', 'RPad', 'RTrim',
+ 'MD5', 'Chr', 'Concat', 'ConcatPair', 'Left', 'Length', 'Lower', 'LPad',
+ 'LTrim', 'Ord', 'Repeat', 'Replace', 'Reverse', 'Right', 'RPad', 'RTrim',
'StrIndex', 'Substr', 'Trim', 'Upper',
# window
'CumeDist', 'DenseRank', 'FirstValue', 'Lag', 'LastValue', 'Lead',
diff --git a/django/db/models/functions/text.py b/django/db/models/functions/text.py
index a79c76a8c9..58f920082b 100644
--- a/django/db/models/functions/text.py
+++ b/django/db/models/functions/text.py
@@ -150,6 +150,22 @@ class LTrim(Transform):
lookup_name = 'ltrim'
+class MD5(Transform):
+ function = 'MD5'
+ lookup_name = 'md5'
+
+ def as_oracle(self, compiler, connection, **extra_context):
+ return super().as_sql(
+ compiler,
+ connection,
+ template=(
+ "LOWER(RAWTOHEX(STANDARD_HASH(UTL_I18N.STRING_TO_RAW("
+ "%(expressions)s, 'AL32UTF8'), '%(function)s')))"
+ ),
+ **extra_context,
+ )
+
+
class Ord(Transform):
function = 'ASCII'
lookup_name = 'ord'
diff --git a/docs/ref/models/database-functions.txt b/docs/ref/models/database-functions.txt
index c7f7b5d981..c6203f92c4 100644
--- a/docs/ref/models/database-functions.txt
+++ b/docs/ref/models/database-functions.txt
@@ -1303,6 +1303,26 @@ Usage example::
Similar to :class:`~django.db.models.functions.Trim`, but removes only leading
spaces.
+``MD5``
+-------
+
+.. class:: MD5(expression, **extra)
+
+.. versionadded:: 3.0
+
+Accepts a single text field or expression and returns the MD5 hash of the
+string.
+
+It can also be registered as a transform as described in :class:`Length`.
+
+Usage example::
+
+ >>> from django.db.models.functions import MD5
+ >>> Author.objects.create(name='Margaret Smith')
+ >>> author = Author.objects.annotate(name_md5=MD5('name')).get()
+ >>> print(author.name_md5)
+ 749fb689816b2db85f5b169c2055b247
+
``Ord``
-------
diff --git a/docs/releases/3.0.txt b/docs/releases/3.0.txt
index 77bace661b..76777a86ef 100644
--- a/docs/releases/3.0.txt
+++ b/docs/releases/3.0.txt
@@ -162,7 +162,7 @@ Migrations
Models
~~~~~~
-* ...
+* Added the :class:`~django.db.models.functions.MD5` database function.
Requests and Responses
~~~~~~~~~~~~~~~~~~~~~~
diff --git a/tests/db_functions/text/test_md5.py b/tests/db_functions/text/test_md5.py
new file mode 100644
index 0000000000..931f80ba2c
--- /dev/null
+++ b/tests/db_functions/text/test_md5.py
@@ -0,0 +1,41 @@
+from django.db import connection
+from django.db.models import CharField
+from django.db.models.functions import MD5
+from django.test import TestCase
+from django.test.utils import register_lookup
+
+from ..models import Author
+
+
+class MD5Tests(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ Author.objects.bulk_create([
+ Author(alias='John Smith'),
+ Author(alias='Jordan Élena'),
+ Author(alias='皇帝'),
+ Author(alias=''),
+ Author(alias=None),
+ ])
+
+ def test_basic(self):
+ authors = Author.objects.annotate(
+ md5_alias=MD5('alias'),
+ ).values_list('md5_alias', flat=True).order_by('pk')
+ self.assertSequenceEqual(
+ authors,
+ [
+ '6117323d2cabbc17d44c2b44587f682c',
+ 'ca6d48f6772000141e66591aee49d56c',
+ 'bf2c13bc1154e3d2e7df848cbc8be73d',
+ 'd41d8cd98f00b204e9800998ecf8427e',
+ 'd41d8cd98f00b204e9800998ecf8427e' if connection.features.interprets_empty_strings_as_nulls else None,
+ ],
+ )
+
+ def test_transform(self):
+ with register_lookup(CharField, MD5):
+ authors = Author.objects.filter(
+ alias__md5='6117323d2cabbc17d44c2b44587f682c',
+ ).values_list('alias', flat=True)
+ self.assertSequenceEqual(authors, ['John Smith'])