diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index f7207a2b951aa..9f30b75e6a8eb 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -4,6 +4,7 @@ import datetime import decimal import functools +import hashlib import math import operator import re @@ -217,6 +218,7 @@ def get_new_connection(self, conn_params): 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 ed863b92af97c..88bc43431a80f 100644 --- a/django/db/models/functions/__init__.py +++ b/django/db/models/functions/__init__.py @@ -10,8 +10,9 @@ 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 @@ '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 a79c76a8c930b..58f920082b486 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 c7f7b5d981041..361291a6985c3 100644 --- a/docs/ref/models/database-functions.txt +++ b/docs/ref/models/database-functions.txt @@ -1303,6 +1303,24 @@ Usage example:: Similar to :class:`~django.db.models.functions.Trim`, but removes only leading spaces. +``MD5`` +------- + +.. class:: MD5(expression, **extra) + +Accepts a single text field or expression and returns the MD5 hash for 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 f036969c9252c..72ad727260c86 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 0000000000000..931f80ba2c79b --- /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'])