diff options
| author | Lily Acorn <code@lilyf.org> | 2025-11-15 16:29:12 +0000 |
|---|---|---|
| committer | Jacob Walls <jacobtylerwalls@gmail.com> | 2025-12-12 11:17:08 -0500 |
| commit | accceec9493d08e19d59fa1a59f69c0fdf23bb13 (patch) | |
| tree | 76fb22d06b9fa3074317a477b713c06fa68ddc12 | |
| parent | 8932a677258fbda34f24a1be6f426265148b0003 (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.py | 3 | ||||
| -rw-r--r-- | django/db/backends/mysql/features.py | 12 | ||||
| -rw-r--r-- | django/db/backends/oracle/features.py | 4 | ||||
| -rw-r--r-- | django/db/backends/postgresql/features.py | 5 | ||||
| -rw-r--r-- | django/db/backends/sqlite3/_functions.py | 16 | ||||
| -rw-r--r-- | django/db/backends/sqlite3/features.py | 6 | ||||
| -rw-r--r-- | django/db/models/functions/__init__.py | 4 | ||||
| -rw-r--r-- | django/db/models/functions/uuid.py | 68 | ||||
| -rw-r--r-- | docs/ref/contrib/postgres/functions.txt | 5 | ||||
| -rw-r--r-- | docs/ref/models/database-functions.txt | 33 | ||||
| -rw-r--r-- | docs/releases/6.1.txt | 3 | ||||
| -rw-r--r-- | tests/db_functions/migrations/0002_create_test_models.py | 6 | ||||
| -rw-r--r-- | tests/db_functions/models.py | 4 | ||||
| -rw-r--r-- | tests/db_functions/test_uuid.py | 64 |
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()) |
