diff --git a/django/db/backends/base/operations.py b/django/db/backends/base/operations.py index d8496f7e2c..2319cb6306 100644 --- a/django/db/backends/base/operations.py +++ b/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 diff --git a/django/db/backends/mysql/operations.py b/django/db/backends/mysql/operations.py index 8d5d7bc866..871c533870 100644 --- a/django/db/backends/mysql/operations.py +++ b/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. diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py index 1e5dc70613..cfce38b189 100644 --- a/django/db/backends/oracle/operations.py +++ b/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'" diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 8a105d4f35..608a2b2a95 100644 --- a/django/db/backends/sqlite3/base.py +++ b/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)) diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index bf5ed49719..0d304b6ec6 100644 --- a/django/db/models/expressions.py +++ b/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 diff --git a/django/db/models/functions/__init__.py b/django/db/models/functions/__init__.py index f6cb4eccf3..980ce2675a 100644 --- a/django/db/models/functions/__init__.py +++ b/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', diff --git a/django/db/models/functions/math.py b/django/db/models/functions/math.py index 909a3088bb..304e112225 100644 --- a/django/db/models/functions/math.py +++ b/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' diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 2fedef62fc..04e430a42e 100644 --- a/django/db/models/sql/compiler.py +++ b/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, diff --git a/docs/ref/models/database-functions.txt b/docs/ref/models/database-functions.txt index 3121a36fa9..5d13f47879 100644 --- a/docs/ref/models/database-functions.txt +++ b/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`` --------- diff --git a/docs/releases/3.2.txt b/docs/releases/3.2.txt index ec78dd863a..4987423ff3 100644 --- a/docs/releases/3.2.txt +++ b/docs/releases/3.2.txt @@ -314,6 +314,8 @@ Models :attr:`TextField ` 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` --------------------------- diff --git a/tests/db_functions/math/test_random.py b/tests/db_functions/math/test_random.py new file mode 100644 index 0000000000..6bcf49baf9 --- /dev/null +++ b/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) diff --git a/tests/expressions/tests.py b/tests/expressions/tests.py index 0ae61a6506..0da90553e4 100644 --- a/tests/expressions/tests.py +++ b/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)")