Browse Source

Refs #28643 -- Added MD5 database function.

Thanks Tim Graham, Nick Pope and Simon Charette for reviews.
Mariusz Felisiak 6 years ago
parent
commit
9ff18c08c3

+ 2 - 0
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))

+ 5 - 4
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',

+ 16 - 0
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'

+ 20 - 0
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``
 -------
 

+ 1 - 1
docs/releases/3.0.txt

@@ -162,7 +162,7 @@ Migrations
 Models
 ~~~~~~
 
-* ...
+* Added the :class:`~django.db.models.functions.MD5` database function.
 
 Requests and Responses
 ~~~~~~~~~~~~~~~~~~~~~~

+ 41 - 0
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'])