summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Pope <nick.pope@flightdataservices.com>2020-07-16 23:32:46 +0100
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2020-10-02 06:58:03 +0200
commit06c5d3fafc6aeb96387148726713b611aeba7fa1 (patch)
tree80a366cad2aa8caf22062d6eb5f4487c36a000e2
parentf87b0ecd37e64e7a019d472de37d0789a8790f1f (diff)
Fixed #32060 -- Added Random database function.
-rw-r--r--django/db/backends/base/operations.py4
-rw-r--r--django/db/backends/mysql/operations.py3
-rw-r--r--django/db/backends/oracle/operations.py3
-rw-r--r--django/db/backends/sqlite3/base.py4
-rw-r--r--django/db/models/expressions.py10
-rw-r--r--django/db/models/functions/__init__.py6
-rw-r--r--django/db/models/functions/math.py14
-rw-r--r--django/db/models/sql/compiler.py4
-rw-r--r--docs/ref/models/database-functions.txt9
-rw-r--r--docs/releases/3.2.txt5
-rw-r--r--tests/db_functions/math/test_random.py13
-rw-r--r--tests/expressions/tests.py3
12 files changed, 51 insertions, 27 deletions
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 <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`
---------------------------
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)")