summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLily Acorn <code@lilyf.org>2025-11-15 16:29:12 +0000
committerJacob Walls <jacobtylerwalls@gmail.com>2025-12-12 11:17:08 -0500
commitaccceec9493d08e19d59fa1a59f69c0fdf23bb13 (patch)
tree76fb22d06b9fa3074317a477b713c06fa68ddc12
parent8932a677258fbda34f24a1be6f426265148b0003 (diff)
Fixed #36735 -- Added UUID4 and UUID7 database functions.
Co-authored-by: Mariusz Felisiak <felisiak.mariusz@gmail.com>
-rw-r--r--django/db/backends/base/features.py3
-rw-r--r--django/db/backends/mysql/features.py12
-rw-r--r--django/db/backends/oracle/features.py4
-rw-r--r--django/db/backends/postgresql/features.py5
-rw-r--r--django/db/backends/sqlite3/_functions.py16
-rw-r--r--django/db/backends/sqlite3/features.py6
-rw-r--r--django/db/models/functions/__init__.py4
-rw-r--r--django/db/models/functions/uuid.py68
-rw-r--r--docs/ref/contrib/postgres/functions.txt5
-rw-r--r--docs/ref/models/database-functions.txt33
-rw-r--r--docs/releases/6.1.txt3
-rw-r--r--tests/db_functions/migrations/0002_create_test_models.py6
-rw-r--r--tests/db_functions/models.py4
-rw-r--r--tests/db_functions/test_uuid.py64
14 files changed, 233 insertions, 0 deletions
diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py
index 38af239cbe..15ac4d7ac0 100644
--- a/django/db/backends/base/features.py
+++ b/django/db/backends/base/features.py
@@ -426,6 +426,9 @@ class BaseDatabaseFeatures:
# that should be skipped for this database.
django_test_skips = {}
+ supports_uuid4_function = False
+ supports_uuid7_function = False
+
def __init__(self, connection):
self.connection = connection
diff --git a/django/db/backends/mysql/features.py b/django/db/backends/mysql/features.py
index 3315db6ae9..4f61e2bdf9 100644
--- a/django/db/backends/mysql/features.py
+++ b/django/db/backends/mysql/features.py
@@ -232,3 +232,15 @@ class DatabaseFeatures(BaseDatabaseFeatures):
@cached_property
def supports_any_value(self):
return not self.connection.mysql_is_mariadb
+
+ @cached_property
+ def supports_uuid4_function(self):
+ if self.connection.mysql_is_mariadb:
+ return self.connection.mysql_version >= (11, 7)
+ return False
+
+ @cached_property
+ def supports_uuid7_function(self):
+ if self.connection.mysql_is_mariadb:
+ return self.connection.mysql_version >= (11, 7)
+ return False
diff --git a/django/db/backends/oracle/features.py b/django/db/backends/oracle/features.py
index 50fa2ce7e9..78dc97a2ec 100644
--- a/django/db/backends/oracle/features.py
+++ b/django/db/backends/oracle/features.py
@@ -224,3 +224,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
def supports_tuple_lookups(self):
# Support is known to be missing on 23.2 but available on 23.4.
return self.connection.oracle_version >= (23, 4)
+
+ @cached_property
+ def supports_uuid4_function(self):
+ return self.connection.oracle_version >= (23, 9)
diff --git a/django/db/backends/postgresql/features.py b/django/db/backends/postgresql/features.py
index 30d06c95c5..edca03b191 100644
--- a/django/db/backends/postgresql/features.py
+++ b/django/db/backends/postgresql/features.py
@@ -79,6 +79,11 @@ class DatabaseFeatures(BaseDatabaseFeatures):
}
test_now_utc_template = "STATEMENT_TIMESTAMP() AT TIME ZONE 'UTC'"
insert_test_table_with_defaults = "INSERT INTO {} DEFAULT VALUES"
+ supports_uuid4_function = True
+
+ @cached_property
+ def supports_uuid7_function(self):
+ return self.is_postgresql_18
@cached_property
def django_test_skips(self):
diff --git a/django/db/backends/sqlite3/_functions.py b/django/db/backends/sqlite3/_functions.py
index b38e06eec2..aa4100e83f 100644
--- a/django/db/backends/sqlite3/_functions.py
+++ b/django/db/backends/sqlite3/_functions.py
@@ -27,6 +27,7 @@ from math import (
tan,
)
from re import search as re_search
+from uuid import uuid4
from django.db.backends.utils import (
split_tzname_delta,
@@ -35,6 +36,10 @@ from django.db.backends.utils import (
)
from django.utils import timezone
from django.utils.duration import duration_microseconds
+from django.utils.version import PY314
+
+if PY314:
+ from uuid import uuid7
def register(connection):
@@ -81,6 +86,9 @@ def register(connection):
connection.create_aggregate("VAR_POP", 1, VarPop)
connection.create_aggregate("VAR_SAMP", 1, VarSamp)
connection.create_aggregate("ANY_VALUE", 1, AnyValue)
+ connection.create_function("UUIDV4", 0, _sqlite_uuid4)
+ if PY314:
+ connection.create_function("UUIDV7", 0, _sqlite_uuid7)
# Some math functions are enabled by default in SQLite 3.35+.
sql = "select sqlite_compileoption_used('ENABLE_MATH_FUNCTIONS')"
if not connection.execute(sql).fetchone()[0]:
@@ -496,6 +504,14 @@ def _sqlite_tan(x):
return tan(x)
+def _sqlite_uuid4():
+ return uuid4().hex
+
+
+def _sqlite_uuid7():
+ return uuid7().hex
+
+
class ListAggregate(list):
step = list.append
diff --git a/django/db/backends/sqlite3/features.py b/django/db/backends/sqlite3/features.py
index 09a0b7fc22..668a53928d 100644
--- a/django/db/backends/sqlite3/features.py
+++ b/django/db/backends/sqlite3/features.py
@@ -5,6 +5,7 @@ from django.db import transaction
from django.db.backends.base.features import BaseDatabaseFeatures
from django.db.utils import OperationalError
from django.utils.functional import cached_property
+from django.utils.version import PY314
from .base import Database
@@ -59,6 +60,11 @@ class DatabaseFeatures(BaseDatabaseFeatures):
can_return_columns_from_insert = True
can_return_rows_from_bulk_insert = True
can_return_rows_from_update = True
+ supports_uuid4_function = True
+
+ @cached_property
+ def supports_uuid7_function(self):
+ return PY314
@cached_property
def django_test_skips(self):
diff --git a/django/db/models/functions/__init__.py b/django/db/models/functions/__init__.py
index 2d36c6945a..5464126f5b 100644
--- a/django/db/models/functions/__init__.py
+++ b/django/db/models/functions/__init__.py
@@ -78,6 +78,7 @@ from .text import (
Trim,
Upper,
)
+from .uuid import UUID4, UUID7
from .window import (
CumeDist,
DenseRank,
@@ -178,6 +179,9 @@ __all__ = [
"Substr",
"Trim",
"Upper",
+ # uuid
+ "UUID4",
+ "UUID7",
# window
"CumeDist",
"DenseRank",
diff --git a/django/db/models/functions/uuid.py b/django/db/models/functions/uuid.py
new file mode 100644
index 0000000000..d4b8d7110a
--- /dev/null
+++ b/django/db/models/functions/uuid.py
@@ -0,0 +1,68 @@
+from django.db import NotSupportedError
+from django.db.models.expressions import Func
+from django.db.models.fields import UUIDField
+
+
+class UUID4(Func):
+ function = "UUIDV4"
+ arity = 0
+ output_field = UUIDField()
+
+ def as_sql(self, compiler, connection, **extra_context):
+ if connection.features.supports_uuid4_function:
+ return super().as_sql(compiler, connection, **extra_context)
+ raise NotSupportedError("UUID4 is not supported on this database backend.")
+
+ def as_postgresql(self, compiler, connection, **extra_context):
+ if connection.features.is_postgresql_18:
+ return self.as_sql(compiler, connection, **extra_context)
+ return self.as_sql(
+ compiler, connection, function="GEN_RANDOM_UUID", **extra_context
+ )
+
+ def as_mysql(self, compiler, connection, **extra_context):
+ if not connection.features.supports_uuid4_function:
+ if connection.mysql_is_mariadb:
+ raise NotSupportedError("UUID4 requires MariaDB version 11.7 or later.")
+ raise NotSupportedError("UUID4 is not supported on MySQL.")
+ return self.as_sql(compiler, connection, function="UUID_V4", **extra_context)
+
+ def as_oracle(self, compiler, connection, **extra_context):
+ if not connection.features.supports_uuid4_function:
+ raise NotSupportedError(
+ "UUID4 requires Oracle version 23ai/26ai (23.9) or later."
+ )
+ return self.as_sql(compiler, connection, function="UUID", **extra_context)
+
+
+class UUID7(Func):
+ function = "UUIDV7"
+ arity = 0
+ output_field = UUIDField()
+
+ def as_sql(self, compiler, connection, **extra_context):
+ if connection.features.supports_uuid7_function:
+ return super().as_sql(compiler, connection, **extra_context)
+ raise NotSupportedError("UUID7 is not supported on this database backend.")
+
+ def as_postgresql(self, compiler, connection, **extra_context):
+ if connection.features.supports_uuid7_function:
+ return self.as_sql(compiler, connection, **extra_context)
+ raise NotSupportedError("UUID7 requires PostgreSQL version 18 or later.")
+
+ # PY314: When dropping support for 3.14, remove the entire method.
+ def as_sqlite(self, compiler, connection, **extra_context):
+ if connection.features.supports_uuid7_function:
+ return self.as_sql(compiler, connection, **extra_context)
+ raise NotSupportedError(
+ "UUID7 on SQLite requires Python version 3.14 or later."
+ )
+
+ def as_mysql(self, compiler, connection, **extra_context):
+ if connection.features.supports_uuid7_function:
+ return self.as_sql(
+ compiler, connection, function="UUID_V7", **extra_context
+ )
+ if connection.mysql_is_mariadb:
+ raise NotSupportedError("UUID7 requires MariaDB version 11.7 or later.")
+ raise NotSupportedError("UUID7 is not supported on MySQL.")
diff --git a/docs/ref/contrib/postgres/functions.txt b/docs/ref/contrib/postgres/functions.txt
index 39d74917da..ba73d26f06 100644
--- a/docs/ref/contrib/postgres/functions.txt
+++ b/docs/ref/contrib/postgres/functions.txt
@@ -21,6 +21,11 @@ Usage example:
>>> from django.contrib.postgres.functions import RandomUUID
>>> Article.objects.update(uuid=RandomUUID())
+.. admonition:: Use :class:`django.db.models.functions.UUID4` instead.
+
+ :class:`django.db.models.functions.UUID4` is a cross-database version of
+ this function. ``RandomUUID`` may be deprecated in the future.
+
``TransactionNow``
==================
diff --git a/docs/ref/models/database-functions.txt b/docs/ref/models/database-functions.txt
index 76c6362da9..cf251a758d 100644
--- a/docs/ref/models/database-functions.txt
+++ b/docs/ref/models/database-functions.txt
@@ -1878,6 +1878,39 @@ Usage example:
.. _window-functions:
+UUID functions
+==============
+
+``UUID4``
+---------
+
+.. versionadded:: 6.1
+
+.. class:: UUID4()
+
+Generates a :rfc:`version 4 UUID <9562#section-5.4>`, which is random and is
+generated using a cryptographically secure method.
+
+.. admonition:: Availability
+
+ Available on PostgreSQL, SQLite, MariaDB version 11.7 or later, and Oracle
+ 23ai/26ai (23.9) or later.
+
+``UUID7``
+---------
+
+.. versionadded:: 6.1
+
+.. class:: UUID7()
+
+Generates a :rfc:`version 7 UUID <9562#section-5.7>`, which starts with a
+time-based component.
+
+.. admonition:: Availability
+
+ Available on PostgreSQL version 18 or later, MariaDB version 11.7 or later,
+ and SQLite on Python version 3.14 or later.
+
Window functions
================
diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt
index 587fec10b3..8ab9ef46a8 100644
--- a/docs/releases/6.1.txt
+++ b/docs/releases/6.1.txt
@@ -263,6 +263,9 @@ Models
:ref:`negative array indexing <key-index-and-path-transforms>` on Oracle
21c+.
+* The new :class:`~django.db.models.functions.UUID4` and
+ :class:`~django.db.models.functions.UUID7` database functions were added.
+
Pagination
~~~~~~~~~~
diff --git a/tests/db_functions/migrations/0002_create_test_models.py b/tests/db_functions/migrations/0002_create_test_models.py
index 37ee93f92f..32fa83b910 100644
--- a/tests/db_functions/migrations/0002_create_test_models.py
+++ b/tests/db_functions/migrations/0002_create_test_models.py
@@ -88,4 +88,10 @@ class Migration(migrations.Migration):
("f2", models.FloatField(null=True, blank=True)),
],
),
+ migrations.CreateModel(
+ name="UUIDModel",
+ fields=[
+ ("uuid", models.UUIDField(null=True)),
+ ],
+ ),
]
diff --git a/tests/db_functions/models.py b/tests/db_functions/models.py
index c126f1bae1..4985b4bfca 100644
--- a/tests/db_functions/models.py
+++ b/tests/db_functions/models.py
@@ -55,3 +55,7 @@ class IntegerModel(models.Model):
class FloatModel(models.Model):
f1 = models.FloatField(null=True, blank=True)
f2 = models.FloatField(null=True, blank=True)
+
+
+class UUIDModel(models.Model):
+ uuid = models.UUIDField(null=True)
diff --git a/tests/db_functions/test_uuid.py b/tests/db_functions/test_uuid.py
new file mode 100644
index 0000000000..b595aafc1b
--- /dev/null
+++ b/tests/db_functions/test_uuid.py
@@ -0,0 +1,64 @@
+import uuid
+
+from django.db import NotSupportedError, connection
+from django.db.models.functions import UUID4, UUID7
+from django.test import TestCase
+from django.test.testcases import skipIfDBFeature, skipUnlessDBFeature
+
+from .models import UUIDModel
+
+
+class TestUUID(TestCase):
+ @skipUnlessDBFeature("supports_uuid4_function")
+ def test_uuid4(self):
+ m1 = UUIDModel.objects.create()
+ m2 = UUIDModel.objects.create()
+ UUIDModel.objects.update(uuid=UUID4())
+ m1.refresh_from_db()
+ m2.refresh_from_db()
+ self.assertIsInstance(m1.uuid, uuid.UUID)
+ self.assertEqual(m1.uuid.version, 4)
+ self.assertNotEqual(m1.uuid, m2.uuid)
+
+ @skipUnlessDBFeature("supports_uuid7_function")
+ def test_uuid7(self):
+ m1 = UUIDModel.objects.create()
+ m2 = UUIDModel.objects.create()
+ UUIDModel.objects.update(uuid=UUID7())
+ m1.refresh_from_db()
+ m2.refresh_from_db()
+ self.assertIsInstance(m1.uuid, uuid.UUID)
+ self.assertEqual(m1.uuid.version, 7)
+ self.assertNotEqual(m1.uuid, m2.uuid)
+
+ @skipIfDBFeature("supports_uuid4_function")
+ def test_uuid4_unsupported(self):
+ if connection.vendor == "mysql":
+ if connection.mysql_is_mariadb:
+ msg = "UUID4 requires MariaDB version 11.7 or later."
+ else:
+ msg = "UUID4 is not supported on MySQL."
+ elif connection.vendor == "oracle":
+ msg = "UUID4 requires Oracle version 23ai/26ai (23.9) or later."
+ else:
+ msg = "UUID4 is not supported on this database backend."
+
+ with self.assertRaisesMessage(NotSupportedError, msg):
+ UUIDModel.objects.update(uuid=UUID4())
+
+ @skipIfDBFeature("supports_uuid7_function")
+ def test_uuid7_unsupported(self):
+ if connection.vendor == "mysql":
+ if connection.mysql_is_mariadb:
+ msg = "UUID7 requires MariaDB version 11.7 or later."
+ else:
+ msg = "UUID7 is not supported on MySQL."
+ elif connection.vendor == "postgresql":
+ msg = "UUID7 requires PostgreSQL version 18 or later."
+ elif connection.vendor == "sqlite":
+ msg = "UUID7 on SQLite requires Python version 3.14 or later."
+ else:
+ msg = "UUID7 is not supported on this database backend."
+
+ with self.assertRaisesMessage(NotSupportedError, msg):
+ UUIDModel.objects.update(uuid=UUID7())