Fixed #32060 -- Added Random database function.

This commit is contained in:
Nick Pope 2020-07-16 23:32:46 +01:00 committed by Mariusz Felisiak
parent f87b0ecd37
commit 06c5d3fafc
12 changed files with 51 additions and 27 deletions

View File

@ -334,10 +334,6 @@ class BaseDatabaseOperations:
""" """
raise NotImplementedError('subclasses of BaseDatabaseOperations may require a quote_name() method') raise NotImplementedError('subclasses of BaseDatabaseOperations may require a quote_name() method')
def random_function_sql(self):
"""Return an SQL expression that returns a random value."""
return 'RANDOM()'
def regex_lookup(self, lookup_type): def regex_lookup(self, lookup_type):
""" """
Return the string to use in a query when performing regular expression Return the string to use in a query when performing regular expression

View File

@ -174,9 +174,6 @@ class DatabaseOperations(BaseDatabaseOperations):
return name # Quoting once is enough. return name # Quoting once is enough.
return "`%s`" % name return "`%s`" % name
def random_function_sql(self):
return 'RAND()'
def return_insert_columns(self, fields): def return_insert_columns(self, fields):
# MySQL and MariaDB < 10.5.0 don't support an INSERT...RETURNING # MySQL and MariaDB < 10.5.0 don't support an INSERT...RETURNING
# statement. # statement.

View File

@ -341,9 +341,6 @@ END;
name = name.replace('%', '%%') name = name.replace('%', '%%')
return name.upper() return name.upper()
def random_function_sql(self):
return "DBMS_RANDOM.RANDOM"
def regex_lookup(self, lookup_type): def regex_lookup(self, lookup_type):
if lookup_type == 'regex': if lookup_type == 'regex':
match_option = "'c'" match_option = "'c'"

View File

@ -7,6 +7,7 @@ import functools
import hashlib import hashlib
import math import math
import operator import operator
import random
import re import re
import statistics import statistics
import warnings import warnings
@ -254,6 +255,9 @@ class DatabaseWrapper(BaseDatabaseWrapper):
create_deterministic_function('SIN', 1, none_guard(math.sin)) create_deterministic_function('SIN', 1, none_guard(math.sin))
create_deterministic_function('SQRT', 1, none_guard(math.sqrt)) create_deterministic_function('SQRT', 1, none_guard(math.sqrt))
create_deterministic_function('TAN', 1, none_guard(math.tan)) create_deterministic_function('TAN', 1, none_guard(math.tan))
# Don't use the built-in RANDOM() function because it returns a value
# in the range [2^63, 2^63 - 1] instead of [0, 1).
conn.create_function('RAND', 0, random.random)
conn.create_aggregate('STDDEV_POP', 1, list_aggregate(statistics.pstdev)) conn.create_aggregate('STDDEV_POP', 1, list_aggregate(statistics.pstdev))
conn.create_aggregate('STDDEV_SAMP', 1, list_aggregate(statistics.stdev)) conn.create_aggregate('STDDEV_SAMP', 1, list_aggregate(statistics.stdev))
conn.create_aggregate('VAR_POP', 1, list_aggregate(statistics.pvariance)) conn.create_aggregate('VAR_POP', 1, list_aggregate(statistics.pvariance))

View File

@ -811,16 +811,6 @@ class Star(Expression):
return '*', [] return '*', []
class Random(Expression):
output_field = fields.FloatField()
def __repr__(self):
return "Random()"
def as_sql(self, compiler, connection):
return connection.ops.random_function_sql(), []
class Col(Expression): class Col(Expression):
contains_column_references = True contains_column_references = True

View File

@ -8,7 +8,7 @@ from .datetime import (
) )
from .math import ( from .math import (
Abs, ACos, ASin, ATan, ATan2, Ceil, Cos, Cot, Degrees, Exp, Floor, Ln, Log, Abs, ACos, ASin, ATan, ATan2, Ceil, Cos, Cot, Degrees, Exp, Floor, Ln, Log,
Mod, Pi, Power, Radians, Round, Sign, Sin, Sqrt, Tan, Mod, Pi, Power, Radians, Random, Round, Sign, Sin, Sqrt, Tan,
) )
from .text import ( from .text import (
MD5, SHA1, SHA224, SHA256, SHA384, SHA512, Chr, Concat, ConcatPair, Left, MD5, SHA1, SHA224, SHA256, SHA384, SHA512, Chr, Concat, ConcatPair, Left,
@ -31,8 +31,8 @@ __all__ = [
'TruncQuarter', 'TruncSecond', 'TruncTime', 'TruncWeek', 'TruncYear', 'TruncQuarter', 'TruncSecond', 'TruncTime', 'TruncWeek', 'TruncYear',
# math # math
'Abs', 'ACos', 'ASin', 'ATan', 'ATan2', 'Ceil', 'Cos', 'Cot', 'Degrees', 'Abs', 'ACos', 'ASin', 'ATan', 'ATan2', 'Ceil', 'Cos', 'Cot', 'Degrees',
'Exp', 'Floor', 'Ln', 'Log', 'Mod', 'Pi', 'Power', 'Radians', 'Round', 'Exp', 'Floor', 'Ln', 'Log', 'Mod', 'Pi', 'Power', 'Radians', 'Random',
'Sign', 'Sin', 'Sqrt', 'Tan', 'Round', 'Sign', 'Sin', 'Sqrt', 'Tan',
# text # text
'MD5', 'SHA1', 'SHA224', 'SHA256', 'SHA384', 'SHA512', 'Chr', 'Concat', 'MD5', 'SHA1', 'SHA224', 'SHA256', 'SHA384', 'SHA512', 'Chr', 'Concat',
'ConcatPair', 'Left', 'Length', 'Lower', 'LPad', 'LTrim', 'Ord', 'Repeat', 'ConcatPair', 'Left', 'Length', 'Lower', 'LPad', 'LTrim', 'Ord', 'Repeat',

View File

@ -141,6 +141,20 @@ class Radians(NumericOutputFieldMixin, Transform):
) )
class Random(NumericOutputFieldMixin, Func):
function = 'RANDOM'
arity = 0
def as_mysql(self, compiler, connection, **extra_context):
return super().as_sql(compiler, connection, function='RAND', **extra_context)
def as_oracle(self, compiler, connection, **extra_context):
return super().as_sql(compiler, connection, function='DBMS_RANDOM.VALUE', **extra_context)
def as_sqlite(self, compiler, connection, **extra_context):
return super().as_sql(compiler, connection, function='RAND', **extra_context)
class Round(Transform): class Round(Transform):
function = 'ROUND' function = 'ROUND'
lookup_name = 'round' lookup_name = 'round'

View File

@ -6,8 +6,8 @@ from itertools import chain
from django.core.exceptions import EmptyResultSet, FieldError from django.core.exceptions import EmptyResultSet, FieldError
from django.db import DatabaseError, NotSupportedError from django.db import DatabaseError, NotSupportedError
from django.db.models.constants import LOOKUP_SEP from django.db.models.constants import LOOKUP_SEP
from django.db.models.expressions import F, OrderBy, Random, RawSQL, Ref, Value from django.db.models.expressions import F, OrderBy, RawSQL, Ref, Value
from django.db.models.functions import Cast from django.db.models.functions import Cast, Random
from django.db.models.query_utils import Q, select_related_descend from django.db.models.query_utils import Q, select_related_descend
from django.db.models.sql.constants import ( from django.db.models.sql.constants import (
CURSOR, GET_ITERATOR_CHUNK_SIZE, MULTI, NO_RESULTS, ORDER_DIR, SINGLE, CURSOR, GET_ITERATOR_CHUNK_SIZE, MULTI, NO_RESULTS, ORDER_DIR, SINGLE,

View File

@ -1114,6 +1114,15 @@ It can also be registered as a transform. For example::
>>> # Get vectors whose radians are less than 1 >>> # Get vectors whose radians are less than 1
>>> vectors = Vector.objects.filter(x__radians__lt=1, y__radians__lt=1) >>> vectors = Vector.objects.filter(x__radians__lt=1, y__radians__lt=1)
``Random``
----------
.. class:: Random(**extra)
.. versionadded:: 3.2
Returns a random value in the range ``0.0 ≤ x < 1.0``.
``Round`` ``Round``
--------- ---------

View File

@ -314,6 +314,8 @@ Models
:attr:`TextField <django.db.models.TextField.db_collation>` allows setting a :attr:`TextField <django.db.models.TextField.db_collation>` allows setting a
database collation for the field. database collation for the field.
* Added the :class:`~django.db.models.functions.Random` database function.
Pagination Pagination
~~~~~~~~~~ ~~~~~~~~~~
@ -444,6 +446,9 @@ backends.
non-deterministic collations are not supported, set non-deterministic collations are not supported, set
``supports_non_deterministic_collations`` to ``False``. ``supports_non_deterministic_collations`` to ``False``.
* ``DatabaseOperations.random_function_sql()`` is removed in favor of the new
:class:`~django.db.models.functions.Random` database function.
:mod:`django.contrib.admin` :mod:`django.contrib.admin`
--------------------------- ---------------------------

View File

@ -0,0 +1,13 @@
from django.db.models.functions import Random
from django.test import TestCase
from ..models import FloatModel
class RandomTests(TestCase):
def test(self):
FloatModel.objects.create()
obj = FloatModel.objects.annotate(random=Random()).first()
self.assertIsInstance(obj.random, float)
self.assertGreaterEqual(obj.random, 0)
self.assertLess(obj.random, 1)

View File

@ -16,7 +16,7 @@ from django.db.models import (
UUIDField, Value, Variance, When, UUIDField, Value, Variance, When,
) )
from django.db.models.expressions import ( from django.db.models.expressions import (
Col, Combinable, CombinedExpression, Random, RawSQL, Ref, Col, Combinable, CombinedExpression, RawSQL, Ref,
) )
from django.db.models.functions import ( from django.db.models.functions import (
Coalesce, Concat, Left, Length, Lower, Substr, Upper, Coalesce, Concat, Left, Length, Lower, Substr, Upper,
@ -1814,7 +1814,6 @@ class ReprTests(SimpleTestCase):
) )
self.assertEqual(repr(Func('published', function='TO_CHAR')), "Func(F(published), function=TO_CHAR)") self.assertEqual(repr(Func('published', function='TO_CHAR')), "Func(F(published), function=TO_CHAR)")
self.assertEqual(repr(OrderBy(Value(1))), 'OrderBy(Value(1), descending=False)') self.assertEqual(repr(OrderBy(Value(1))), 'OrderBy(Value(1), descending=False)')
self.assertEqual(repr(Random()), "Random()")
self.assertEqual(repr(RawSQL('table.col', [])), "RawSQL(table.col, [])") self.assertEqual(repr(RawSQL('table.col', [])), "RawSQL(table.col, [])")
self.assertEqual(repr(Ref('sum_cost', Sum('cost'))), "Ref(sum_cost, Sum(F(cost)))") self.assertEqual(repr(Ref('sum_cost', Sum('cost'))), "Ref(sum_cost, Sum(F(cost)))")
self.assertEqual(repr(Value(1)), "Value(1)") self.assertEqual(repr(Value(1)), "Value(1)")