summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLily Acorn <code@lilyf.org>2025-11-20 20:10:13 +0000
committerJacob Walls <jacobtylerwalls@gmail.com>2025-12-12 11:50:36 -0500
commite95468ed97b1c250939da316218c39dad577ba32 (patch)
treec4059a0fa69e071b8d87d918cf01791d7f381af1
parentaccceec9493d08e19d59fa1a59f69c0fdf23bb13 (diff)
Refs #36735 -- Supported shift parameter for UUID7 on PostgreSQL.
-rw-r--r--django/db/backends/base/features.py1
-rw-r--r--django/db/backends/postgresql/features.py4
-rw-r--r--django/db/models/functions/uuid.py25
-rw-r--r--docs/ref/models/database-functions.txt10
-rw-r--r--tests/db_functions/migrations/0002_create_test_models.py1
-rw-r--r--tests/db_functions/models.py1
-rw-r--r--tests/db_functions/test_uuid.py27
7 files changed, 64 insertions, 5 deletions
diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py
index 15ac4d7ac0..a28d622bac 100644
--- a/django/db/backends/base/features.py
+++ b/django/db/backends/base/features.py
@@ -428,6 +428,7 @@ class BaseDatabaseFeatures:
supports_uuid4_function = False
supports_uuid7_function = False
+ supports_uuid7_function_shift = False
def __init__(self, connection):
self.connection = connection
diff --git a/django/db/backends/postgresql/features.py b/django/db/backends/postgresql/features.py
index edca03b191..7d313467d4 100644
--- a/django/db/backends/postgresql/features.py
+++ b/django/db/backends/postgresql/features.py
@@ -86,6 +86,10 @@ class DatabaseFeatures(BaseDatabaseFeatures):
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):
skips = {
"opclasses are PostgreSQL only.": {
diff --git a/django/db/models/functions/uuid.py b/django/db/models/functions/uuid.py
index d4b8d7110a..7059798ff3 100644
--- a/django/db/models/functions/uuid.py
+++ b/django/db/models/functions/uuid.py
@@ -37,13 +37,30 @@ class UUID4(Func):
class UUID7(Func):
function = "UUIDV7"
- arity = 0
+ 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 connection.features.supports_uuid7_function:
- return super().as_sql(compiler, connection, **extra_context)
- raise NotSupportedError("UUID7 is not supported on this database backend.")
+ 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:
diff --git a/docs/ref/models/database-functions.txt b/docs/ref/models/database-functions.txt
index cf251a758d..fba7a53826 100644
--- a/docs/ref/models/database-functions.txt
+++ b/docs/ref/models/database-functions.txt
@@ -1901,7 +1901,7 @@ generated using a cryptographically secure method.
.. versionadded:: 6.1
-.. class:: UUID7()
+.. class:: UUID7(shift=None)
Generates a :rfc:`version 7 UUID <9562#section-5.7>`, which starts with a
time-based component.
@@ -1911,6 +1911,14 @@ time-based component.
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
================
diff --git a/tests/db_functions/migrations/0002_create_test_models.py b/tests/db_functions/migrations/0002_create_test_models.py
index 32fa83b910..6c4626e7ea 100644
--- a/tests/db_functions/migrations/0002_create_test_models.py
+++ b/tests/db_functions/migrations/0002_create_test_models.py
@@ -92,6 +92,7 @@ class Migration(migrations.Migration):
name="UUIDModel",
fields=[
("uuid", models.UUIDField(null=True)),
+ ("shift", models.DurationField(null=True)),
],
),
]
diff --git a/tests/db_functions/models.py b/tests/db_functions/models.py
index 4985b4bfca..45a5a027f4 100644
--- a/tests/db_functions/models.py
+++ b/tests/db_functions/models.py
@@ -59,3 +59,4 @@ class FloatModel(models.Model):
class UUIDModel(models.Model):
uuid = models.UUIDField(null=True)
+ shift = models.DurationField(null=True)
diff --git a/tests/db_functions/test_uuid.py b/tests/db_functions/test_uuid.py
index b595aafc1b..4071071d2d 100644
--- a/tests/db_functions/test_uuid.py
+++ b/tests/db_functions/test_uuid.py
@@ -1,4 +1,5 @@
import uuid
+from datetime import datetime, timedelta, timezone
from django.db import NotSupportedError, connection
from django.db.models.functions import UUID4, UUID7
@@ -31,6 +32,24 @@ class TestUUID(TestCase):
self.assertEqual(m1.uuid.version, 7)
self.assertNotEqual(m1.uuid, m2.uuid)
+ @skipUnlessDBFeature("supports_uuid7_function_shift")
+ def test_uuid7_shift(self):
+ now = datetime.now(timezone.utc)
+ past = datetime(2005, 11, 16, tzinfo=timezone.utc)
+ shift = past - now
+ m = UUIDModel.objects.create(uuid=UUID7(shift))
+ self.assertTrue(str(m.uuid).startswith("0107965e-e40"), m.uuid)
+
+ @skipUnlessDBFeature("supports_uuid7_function_shift")
+ def test_uuid7_shift_duration_field(self):
+ now = datetime.now(timezone.utc)
+ past = datetime(2005, 11, 16, tzinfo=timezone.utc)
+ shift = past - now
+ m = UUIDModel.objects.create(shift=shift)
+ UUIDModel.objects.update(uuid=UUID7("shift"))
+ m.refresh_from_db()
+ self.assertTrue(str(m.uuid).startswith("0107965e-e40"), m.uuid)
+
@skipIfDBFeature("supports_uuid4_function")
def test_uuid4_unsupported(self):
if connection.vendor == "mysql":
@@ -62,3 +81,11 @@ class TestUUID(TestCase):
with self.assertRaisesMessage(NotSupportedError, msg):
UUIDModel.objects.update(uuid=UUID7())
+
+ @skipUnlessDBFeature("supports_uuid7_function")
+ @skipIfDBFeature("supports_uuid7_function_shift")
+ def test_uuid7_shift_unsupported(self):
+ msg = "The shift argument to UUID7 is not supported on this database backend."
+
+ with self.assertRaisesMessage(NotSupportedError, msg):
+ UUIDModel.objects.update(uuid=UUID7(shift=timedelta(hours=12)))