浏览代码

Fixed #32060 -- Added Random database function.

Nick Pope 4 年之前
父节点
当前提交
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')
         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

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

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

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

@@ -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'"

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

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

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

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

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

@@ -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',
-    'Sign', 'Sin', 'Sqrt', 'Tan',
+    'Exp', 'Floor', 'Ln', 'Log', 'Mod', 'Pi', 'Power', 'Radians', 'Random',
+    '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',

+ 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):
 class Round(Transform):
     function = 'ROUND'
     function = 'ROUND'
     lookup_name = '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.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.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.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,

+ 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
     >>> # 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``
 ---------
 ---------
 
 

+ 5 - 0
docs/releases/3.2.txt

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

+ 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,
     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)")