Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions django/db/backends/base/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,10 @@ class BaseDatabaseFeatures:
# that should be skipped for this database.
django_test_skips = {}

supports_uuid4_function = False
supports_uuid7_function = False
supports_uuid7_function_shift = False

def __init__(self, connection):
self.connection = connection

Expand Down
12 changes: 12 additions & 0 deletions django/db/backends/mysql/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,15 @@ def allows_group_by_selected_pks(self):
@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
4 changes: 4 additions & 0 deletions django/db/backends/oracle/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,7 @@ def bare_select_suffix(self):
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)
9 changes: 9 additions & 0 deletions django/db/backends/postgresql/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ 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 supports_uuid7_function_shift(self):
return self.is_postgresql_18

@cached_property
def django_test_skips(self):
Expand Down
16 changes: 16 additions & 0 deletions django/db/backends/sqlite3/_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
tan,
)
from re import search as re_search
from uuid import uuid4

from django.db.backends.utils import (
split_tzname_delta,
Expand All @@ -35,6 +36,10 @@
)
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):
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions django/db/backends/sqlite3/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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

Expand Down Expand Up @@ -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):
Expand Down
4 changes: 4 additions & 0 deletions django/db/models/functions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
Trim,
Upper,
)
from .uuid import UUID4, UUID7
from .window import (
CumeDist,
DenseRank,
Expand Down Expand Up @@ -178,6 +179,9 @@
"Substr",
"Trim",
"Upper",
# uuid
"UUID4",
"UUID7",
# window
"CumeDist",
"DenseRank",
Expand Down
85 changes: 85 additions & 0 deletions django/db/models/functions/uuid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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 = 1
output_field = UUIDField()

def __init__(self, shift=None, **extra):
super().__init__(shift, **extra)

def _parse_expressions(self, *expressions):
if expressions[0] is None:
expressions = expressions[1:]
return super()._parse_expressions(*expressions)

def as_sql(self, compiler, connection, **extra_context):
if not connection.features.supports_uuid7_function:
raise NotSupportedError("UUID7 is not supported on this database backend.")

if len(self.source_expressions) == 1:
if not connection.features.supports_uuid7_function_shift:
msg = (
"The shift argument to UUID7 is not supported "
"on this database backend."
)
raise NotSupportedError(msg)

return super().as_sql(compiler, connection, **extra_context)

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.")
5 changes: 5 additions & 0 deletions docs/ref/contrib/postgres/functions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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``
==================

Expand Down
41 changes: 41 additions & 0 deletions docs/ref/models/database-functions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1878,6 +1878,47 @@ 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(shift=None)

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.

.. admonition:: ``shift``

On PostgreSQL, ``UUID7`` also supports an optional ``shift`` value, which
can be used to offset the timestamp of the generated UUID. This should be a
:class:`datetime.timedelta` or an expression with an ``output_field`` of
:class:`~django.db.models.DurationField`. A positive value will shift the
timestamp into the future and a negative value into the past.

Window functions
================

Expand Down
3 changes: 3 additions & 0 deletions docs/releases/6.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~

Expand Down
7 changes: 7 additions & 0 deletions tests/db_functions/migrations/0002_create_test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,11 @@ class Migration(migrations.Migration):
("f2", models.FloatField(null=True, blank=True)),
],
),
migrations.CreateModel(
name="UUIDModel",
fields=[
("uuid", models.UUIDField(null=True)),
("shift", models.DurationField(null=True)),
],
),
]
5 changes: 5 additions & 0 deletions tests/db_functions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,8 @@ 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)
shift = models.DurationField(null=True)
Loading