Browse Source

Fixed #32060 -- Added Random database function.

Nick Pope 4 years ago
parent
commit
06c5d3fafc

+ 0 - 4
django/db/backends/base/operations.py

@@ -334,10 +334,6 @@ class BaseDatabaseOperations:
         """
         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):
         """
         Return the string to use in a query when performing regular expression

+ 0 - 3
django/db/backends/mysql/operations.py

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

+ 0 - 3
django/db/backends/oracle/operations.py

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

+ 4 - 0
django/db/backends/sqlite3/base.py

@@ -7,6 +7,7 @@ import functools
 import hashlib
 import math
 import operator
+import random
 import re
 import statistics
 import warnings
@@ -254,6 +255,9 @@ class DatabaseWrapper(BaseDatabaseWrapper):
         create_deterministic_function('SIN', 1, none_guard(math.sin))
         create_deterministic_function('SQRT', 1, none_guard(math.sqrt))
         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_SAMP', 1, list_aggregate(statistics.stdev))
         conn.create_aggregate('VAR_POP', 1, list_aggregate(statistics.pvariance))

+ 0 - 10
django/db/models/expressions.py

@@ -811,16 +811,6 @@ class Star(Expression):
         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):
 
     contains_column_references = True

+ 3 - 3
django/db/models/functions/__init__.py

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

+ 14 - 0
django/db/models/functions/math.py

@@ -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):
     function = 'ROUND'
     lookup_name = 'round'

+ 2 - 2
django/db/models/sql/compiler.py

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

+ 9 - 0
docs/ref/models/database-functions.txt

@@ -1114,6 +1114,15 @@ It can also be registered as a transform. For example::
     >>> # Get vectors whose radians are less than 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``
 ---------
 

+ 5 - 0
docs/releases/3.2.txt

@@ -314,6 +314,8 @@ Models
   :attr:`TextField <django.db.models.TextField.db_collation>` allows setting a
   database collation for the field.
 
+* Added the :class:`~django.db.models.functions.Random` database function.
+
 Pagination
 ~~~~~~~~~~
 
@@ -444,6 +446,9 @@ backends.
   non-deterministic collations are not supported, set
   ``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`
 ---------------------------
 

+ 13 - 0
tests/db_functions/math/test_random.py

@@ -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)

+ 1 - 2
tests/expressions/tests.py

@@ -16,7 +16,7 @@ from django.db.models import (
     UUIDField, Value, Variance, When,
 )
 from django.db.models.expressions import (
-    Col, Combinable, CombinedExpression, Random, RawSQL, Ref,
+    Col, Combinable, CombinedExpression, RawSQL, Ref,
 )
 from django.db.models.functions import (
     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(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(Ref('sum_cost', Sum('cost'))), "Ref(sum_cost, Sum(F(cost)))")
         self.assertEqual(repr(Value(1)), "Value(1)")