Fixed #30240 -- Added SHA1, SHA224, SHA256, SHA384, and SHA512 database functions.

Thanks Mariusz Felisiak and Tim Graham for reviews.
This commit is contained in:
Nick Pope 2019-03-20 18:30:43 +00:00 committed by Mariusz Felisiak
parent 0193bf874f
commit 0b70985f42
13 changed files with 439 additions and 19 deletions

View File

@ -226,6 +226,11 @@ class DatabaseWrapper(BaseDatabaseWrapper):
conn.create_function('REPEAT', 2, none_guard(operator.mul)) conn.create_function('REPEAT', 2, none_guard(operator.mul))
conn.create_function('REVERSE', 1, none_guard(lambda x: x[::-1])) conn.create_function('REVERSE', 1, none_guard(lambda x: x[::-1]))
conn.create_function('RPAD', 3, _sqlite_rpad) conn.create_function('RPAD', 3, _sqlite_rpad)
conn.create_function('SHA1', 1, none_guard(lambda x: hashlib.sha1(x.encode()).hexdigest()))
conn.create_function('SHA224', 1, none_guard(lambda x: hashlib.sha224(x.encode()).hexdigest()))
conn.create_function('SHA256', 1, none_guard(lambda x: hashlib.sha256(x.encode()).hexdigest()))
conn.create_function('SHA384', 1, none_guard(lambda x: hashlib.sha384(x.encode()).hexdigest()))
conn.create_function('SHA512', 1, none_guard(lambda x: hashlib.sha512(x.encode()).hexdigest()))
conn.create_function('SIN', 1, none_guard(math.sin)) conn.create_function('SIN', 1, none_guard(math.sin))
conn.create_function('SQRT', 1, none_guard(math.sqrt)) conn.create_function('SQRT', 1, none_guard(math.sqrt))
conn.create_function('TAN', 1, none_guard(math.tan)) conn.create_function('TAN', 1, none_guard(math.tan))

View File

@ -10,9 +10,9 @@ from .math import (
Mod, Pi, Power, Radians, Round, Sin, Sqrt, Tan, Mod, Pi, Power, Radians, Round, Sin, Sqrt, Tan,
) )
from .text import ( from .text import (
MD5, Chr, Concat, ConcatPair, Left, Length, Lower, LPad, LTrim, Ord, MD5, SHA1, SHA224, SHA256, SHA384, SHA512, Chr, Concat, ConcatPair, Left,
Repeat, Replace, Reverse, Right, RPad, RTrim, StrIndex, Substr, Trim, Length, Lower, LPad, LTrim, Ord, Repeat, Replace, Reverse, Right, RPad,
Upper, RTrim, StrIndex, Substr, Trim, Upper,
) )
from .window import ( from .window import (
CumeDist, DenseRank, FirstValue, Lag, LastValue, Lead, NthValue, Ntile, CumeDist, DenseRank, FirstValue, Lag, LastValue, Lead, NthValue, Ntile,
@ -34,9 +34,10 @@ __all__ = [
'Exp', 'Floor', 'Ln', 'Log', 'Mod', 'Pi', 'Power', 'Radians', 'Round', 'Exp', 'Floor', 'Ln', 'Log', 'Mod', 'Pi', 'Power', 'Radians', 'Round',
'Sin', 'Sqrt', 'Tan', 'Sin', 'Sqrt', 'Tan',
# text # text
'MD5', 'Chr', 'Concat', 'ConcatPair', 'Left', 'Length', 'Lower', 'LPad', 'MD5', 'SHA1', 'SHA224', 'SHA256', 'SHA384', 'SHA512', 'Chr', 'Concat',
'LTrim', 'Ord', 'Repeat', 'Replace', 'Reverse', 'Right', 'RPad', 'RTrim', 'ConcatPair', 'Left', 'Length', 'Lower', 'LPad', 'LTrim', 'Ord', 'Repeat',
'StrIndex', 'Substr', 'Trim', 'Upper', 'Replace', 'Reverse', 'Right', 'RPad', 'RTrim', 'StrIndex', 'Substr',
'Trim', 'Upper',
# window # window
'CumeDist', 'DenseRank', 'FirstValue', 'Lag', 'LastValue', 'Lead', 'CumeDist', 'DenseRank', 'FirstValue', 'Lag', 'LastValue', 'Lead',
'NthValue', 'Ntile', 'PercentRank', 'Rank', 'RowNumber', 'NthValue', 'Ntile', 'PercentRank', 'Rank', 'RowNumber',

View File

@ -2,6 +2,7 @@ from django.db.models.expressions import Func, Value
from django.db.models.fields import IntegerField from django.db.models.fields import IntegerField
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.db.models.lookups import Transform from django.db.models.lookups import Transform
from django.db.utils import NotSupportedError
class BytesToCharFieldConversionMixin: class BytesToCharFieldConversionMixin:
@ -20,6 +21,40 @@ class BytesToCharFieldConversionMixin:
return super().convert_value(value, expression, connection) return super().convert_value(value, expression, connection)
class MySQLSHA2Mixin:
def as_mysql(self, compiler, connection, **extra_content):
return super().as_sql(
compiler,
connection,
template='SHA2(%%(expressions)s, %s)' % self.function[3:],
**extra_content,
)
class OracleHashMixin:
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 PostgreSQLSHAMixin:
def as_postgresql(self, compiler, connection, **extra_content):
return super().as_sql(
compiler,
connection,
template="ENCODE(DIGEST(%(expressions)s, '%(function)s'), 'hex')",
function=self.function.lower(),
**extra_content,
)
class Chr(Transform): class Chr(Transform):
function = 'CHR' function = 'CHR'
lookup_name = 'chr' lookup_name = 'chr'
@ -150,21 +185,10 @@ class LTrim(Transform):
lookup_name = 'ltrim' lookup_name = 'ltrim'
class MD5(Transform): class MD5(OracleHashMixin, Transform):
function = 'MD5' function = 'MD5'
lookup_name = '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): class Ord(Transform):
function = 'ASCII' function = 'ASCII'
@ -235,6 +259,34 @@ class RTrim(Transform):
lookup_name = 'rtrim' lookup_name = 'rtrim'
class SHA1(OracleHashMixin, PostgreSQLSHAMixin, Transform):
function = 'SHA1'
lookup_name = 'sha1'
class SHA224(MySQLSHA2Mixin, PostgreSQLSHAMixin, Transform):
function = 'SHA224'
lookup_name = 'sha224'
def as_oracle(self, compiler, connection, **extra_context):
raise NotSupportedError('SHA224 is not supported on Oracle.')
class SHA256(MySQLSHA2Mixin, OracleHashMixin, PostgreSQLSHAMixin, Transform):
function = 'SHA256'
lookup_name = 'sha256'
class SHA384(MySQLSHA2Mixin, OracleHashMixin, PostgreSQLSHAMixin, Transform):
function = 'SHA384'
lookup_name = 'sha384'
class SHA512(MySQLSHA2Mixin, OracleHashMixin, PostgreSQLSHAMixin, Transform):
function = 'SHA512'
lookup_name = 'sha512'
class StrIndex(Func): class StrIndex(Func):
""" """
Return a positive integer corresponding to the 1-indexed position of the Return a positive integer corresponding to the 1-indexed position of the

View File

@ -1441,6 +1441,41 @@ side.
Similar to :class:`~django.db.models.functions.Trim`, but removes only trailing Similar to :class:`~django.db.models.functions.Trim`, but removes only trailing
spaces. spaces.
``SHA1``, ``SHA224``, ``SHA256``, ``SHA384``, and ``SHA512``
------------------------------------------------------------
.. class:: SHA1(expression, **extra)
.. class:: SHA224(expression, **extra)
.. class:: SHA256(expression, **extra)
.. class:: SHA384(expression, **extra)
.. class:: SHA512(expression, **extra)
.. versionadded:: 3.0
Accepts a single text field or expression and returns the particular hash of
the string.
They can also be registered as transforms as described in :class:`Length`.
Usage example::
>>> from django.db.models.functions import SHA1
>>> Author.objects.create(name='Margaret Smith')
>>> author = Author.objects.annotate(name_sha1=SHA1('name')).get()
>>> print(author.name_sha1)
b87efd8a6c991c390be5a68e8a7945a7851c7e5c
.. admonition:: PostgreSQL
The `pgcrypto extension <https://www.postgresql.org/docs/current/static/
pgcrypto.html>`_ must be installed. You can use the
:class:`~django.contrib.postgres.operations.CryptoExtension` migration
operation to install it.
.. admonition:: Oracle
Oracle doesn't support the ``SHA224`` function.
``StrIndex`` ``StrIndex``
------------ ------------

View File

@ -168,7 +168,12 @@ Migrations
Models Models
~~~~~~ ~~~~~~
* Added the :class:`~django.db.models.functions.MD5` database function. * Added hash database functions :class:`~django.db.models.functions.MD5`,
:class:`~django.db.models.functions.SHA1`,
:class:`~django.db.models.functions.SHA224`,
:class:`~django.db.models.functions.SHA256`,
:class:`~django.db.models.functions.SHA384`, and
:class:`~django.db.models.functions.SHA512`.
* The new ``is_dst`` parameter of the * The new ``is_dst`` parameter of the
:class:`~django.db.models.functions.Trunc` database functions determines the :class:`~django.db.models.functions.Trunc` database functions determines the

View File

@ -0,0 +1,13 @@
from unittest import mock
from django.db import migrations
try:
from django.contrib.postgres.operations import CryptoExtension
except ImportError:
CryptoExtension = mock.Mock()
class Migration(migrations.Migration):
# Required for the SHA database functions.
operations = [CryptoExtension()]

View File

@ -0,0 +1,77 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('db_functions', '0001_setup_extensions'),
]
operations = [
migrations.CreateModel(
name='Author',
fields=[
('name', models.CharField(max_length=50)),
('alias', models.CharField(max_length=50, null=True, blank=True)),
('goes_by', models.CharField(max_length=50, null=True, blank=True)),
('age', models.PositiveSmallIntegerField(default=30)),
],
),
migrations.CreateModel(
name='Article',
fields=[
('authors', models.ManyToManyField('db_functions.Author', related_name='articles')),
('title', models.CharField(max_length=50)),
('summary', models.CharField(max_length=200, null=True, blank=True)),
('text', models.TextField()),
('written', models.DateTimeField()),
('published', models.DateTimeField(null=True, blank=True)),
('updated', models.DateTimeField(null=True, blank=True)),
('views', models.PositiveIntegerField(default=0)),
],
),
migrations.CreateModel(
name='Fan',
fields=[
('name', models.CharField(max_length=50)),
('age', models.PositiveSmallIntegerField(default=30)),
('author', models.ForeignKey('db_functions.Author', models.CASCADE, related_name='fans')),
('fan_since', models.DateTimeField(null=True, blank=True)),
],
),
migrations.CreateModel(
name='DTModel',
fields=[
('name', models.CharField(max_length=32)),
('start_datetime', models.DateTimeField(null=True, blank=True)),
('end_datetime', models.DateTimeField(null=True, blank=True)),
('start_date', models.DateField(null=True, blank=True)),
('end_date', models.DateField(null=True, blank=True)),
('start_time', models.TimeField(null=True, blank=True)),
('end_time', models.TimeField(null=True, blank=True)),
('duration', models.DurationField(null=True, blank=True)),
],
),
migrations.CreateModel(
name='DecimalModel',
fields=[
('n1', models.DecimalField(decimal_places=2, max_digits=6)),
('n2', models.DecimalField(decimal_places=2, max_digits=6)),
],
),
migrations.CreateModel(
name='IntegerModel',
fields=[
('big', models.BigIntegerField(null=True, blank=True)),
('normal', models.IntegerField(null=True, blank=True)),
('small', models.SmallIntegerField(null=True, blank=True)),
],
),
migrations.CreateModel(
name='FloatModel',
fields=[
('f1', models.FloatField(null=True, blank=True)),
('f2', models.FloatField(null=True, blank=True)),
],
),
]

View File

@ -0,0 +1,42 @@
from django.db import connection
from django.db.models import CharField
from django.db.models.functions import SHA1
from django.test import TestCase
from django.test.utils import register_lookup
from ..models import Author
class SHA1Tests(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(
sha1_alias=SHA1('alias'),
).values_list('sha1_alias', flat=True).order_by('pk')
self.assertSequenceEqual(
authors,
[
'e61a3587b3f7a142b8c7b9263c82f8119398ecb7',
'0781e0745a2503e6ded05ed5bc554c421d781b0c',
'198d15ea139de04060caf95bc3e0ec5883cba881',
'da39a3ee5e6b4b0d3255bfef95601890afd80709',
'da39a3ee5e6b4b0d3255bfef95601890afd80709'
if connection.features.interprets_empty_strings_as_nulls else None,
],
)
def test_transform(self):
with register_lookup(CharField, SHA1):
authors = Author.objects.filter(
alias__sha1='e61a3587b3f7a142b8c7b9263c82f8119398ecb7',
).values_list('alias', flat=True)
self.assertSequenceEqual(authors, ['John Smith'])

View File

@ -0,0 +1,53 @@
import unittest
from django.db import connection
from django.db.models import CharField
from django.db.models.functions import SHA224
from django.db.utils import NotSupportedError
from django.test import TestCase
from django.test.utils import register_lookup
from ..models import Author
class SHA224Tests(TestCase):
@classmethod
def setUpTestData(cls):
Author.objects.bulk_create([
Author(alias='John Smith'),
Author(alias='Jordan Élena'),
Author(alias='皇帝'),
Author(alias=''),
Author(alias=None),
])
@unittest.skipIf(connection.vendor == 'oracle', "Oracle doesn't support SHA224.")
def test_basic(self):
authors = Author.objects.annotate(
sha224_alias=SHA224('alias'),
).values_list('sha224_alias', flat=True).order_by('pk')
self.assertSequenceEqual(
authors,
[
'a61303c220731168452cb6acf3759438b1523e768f464e3704e12f70',
'2297904883e78183cb118fc3dc21a610d60daada7b6ebdbc85139f4d',
'eba942746e5855121d9d8f79e27dfdebed81adc85b6bf41591203080',
'd14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f',
'd14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f'
if connection.features.interprets_empty_strings_as_nulls else None,
],
)
@unittest.skipIf(connection.vendor == 'oracle', "Oracle doesn't support SHA224.")
def test_transform(self):
with register_lookup(CharField, SHA224):
authors = Author.objects.filter(
alias__sha224='a61303c220731168452cb6acf3759438b1523e768f464e3704e12f70',
).values_list('alias', flat=True)
self.assertSequenceEqual(authors, ['John Smith'])
@unittest.skipUnless(connection.vendor == 'oracle', "Oracle doesn't support SHA224.")
def test_unsupported(self):
msg = 'SHA224 is not supported on Oracle.'
with self.assertRaisesMessage(NotSupportedError, msg):
Author.objects.annotate(sha224_alias=SHA224('alias')).first()

View File

@ -0,0 +1,42 @@
from django.db import connection
from django.db.models import CharField
from django.db.models.functions import SHA256
from django.test import TestCase
from django.test.utils import register_lookup
from ..models import Author
class SHA256Tests(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(
sha256_alias=SHA256('alias'),
).values_list('sha256_alias', flat=True).order_by('pk')
self.assertSequenceEqual(
authors,
[
'ef61a579c907bbed674c0dbcbcf7f7af8f851538eef7b8e58c5bee0b8cfdac4a',
'6e4cce20cd83fc7c202f21a8b2452a68509cf24d1c272a045b5e0cfc43f0d94e',
'3ad2039e3ec0c88973ae1c0fce5a3dbafdd5a1627da0a92312c54ebfcf43988e',
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
if connection.features.interprets_empty_strings_as_nulls else None,
],
)
def test_transform(self):
with register_lookup(CharField, SHA256):
authors = Author.objects.filter(
alias__sha256='ef61a579c907bbed674c0dbcbcf7f7af8f851538eef7b8e58c5bee0b8cfdac4a',
).values_list('alias', flat=True)
self.assertSequenceEqual(authors, ['John Smith'])

View File

@ -0,0 +1,44 @@
from django.db import connection
from django.db.models import CharField
from django.db.models.functions import SHA384
from django.test import TestCase
from django.test.utils import register_lookup
from ..models import Author
class SHA384Tests(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(
sha384_alias=SHA384('alias'),
).values_list('sha384_alias', flat=True).order_by('pk')
self.assertSequenceEqual(
authors,
[
'9df976bfbcf96c66fbe5cba866cd4deaa8248806f15b69c4010a404112906e4ca7b57e53b9967b80d77d4f5c2982cbc8',
'72202c8005492016cc670219cce82d47d6d2d4273464c742ab5811d691b1e82a7489549e3a73ffa119694f90678ba2e3',
'eda87fae41e59692c36c49e43279c8111a00d79122a282a944e8ba9a403218f049a48326676a43c7ba378621175853b0',
'38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b',
'38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b'
if connection.features.interprets_empty_strings_as_nulls else None,
],
)
def test_transform(self):
with register_lookup(CharField, SHA384):
authors = Author.objects.filter(
alias__sha384=(
'9df976bfbcf96c66fbe5cba866cd4deaa8248806f15b69c4010a404112906e4ca7b57e53b9967b80d77d4f5c2982cbc8'
),
).values_list('alias', flat=True)
self.assertSequenceEqual(authors, ['John Smith'])

View File

@ -0,0 +1,51 @@
from django.db import connection
from django.db.models import CharField
from django.db.models.functions import SHA512
from django.test import TestCase
from django.test.utils import register_lookup
from ..models import Author
class SHA512Tests(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(
sha512_alias=SHA512('alias'),
).values_list('sha512_alias', flat=True).order_by('pk')
self.assertSequenceEqual(
authors,
[
'ed014a19bb67a85f9c8b1d81e04a0e7101725be8627d79d02ca4f3bd803f33cf'
'3b8fed53e80d2a12c0d0e426824d99d110f0919298a5055efff040a3fc091518',
'b09c449f3ba49a32ab44754982d4749ac938af293e4af2de28858858080a1611'
'2b719514b5e48cb6ce54687e843a4b3e69a04cdb2a9dc99c3b99bdee419fa7d0',
'b554d182e25fb487a3f2b4285bb8672f98956b5369138e681b467d1f079af116'
'172d88798345a3a7666faf5f35a144c60812d3234dcd35f444624f2faee16857',
'cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce'
'47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e',
'cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce'
'47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e'
if connection.features.interprets_empty_strings_as_nulls else None,
],
)
def test_transform(self):
with register_lookup(CharField, SHA512):
authors = Author.objects.filter(
alias__sha512=(
'ed014a19bb67a85f9c8b1d81e04a0e7101725be8627d79d02ca4f3bd8'
'03f33cf3b8fed53e80d2a12c0d0e426824d99d110f0919298a5055eff'
'f040a3fc091518'
),
).values_list('alias', flat=True)
self.assertSequenceEqual(authors, ['John Smith'])