diff options
| author | Lily Acorn <code@lilyf.org> | 2025-11-20 20:10:13 +0000 |
|---|---|---|
| committer | Jacob Walls <jacobtylerwalls@gmail.com> | 2025-12-12 11:50:36 -0500 |
| commit | e95468ed97b1c250939da316218c39dad577ba32 (patch) | |
| tree | c4059a0fa69e071b8d87d918cf01791d7f381af1 | |
| parent | accceec9493d08e19d59fa1a59f69c0fdf23bb13 (diff) | |
Refs #36735 -- Supported shift parameter for UUID7 on PostgreSQL.
| -rw-r--r-- | django/db/backends/base/features.py | 1 | ||||
| -rw-r--r-- | django/db/backends/postgresql/features.py | 4 | ||||
| -rw-r--r-- | django/db/models/functions/uuid.py | 25 | ||||
| -rw-r--r-- | docs/ref/models/database-functions.txt | 10 | ||||
| -rw-r--r-- | tests/db_functions/migrations/0002_create_test_models.py | 1 | ||||
| -rw-r--r-- | tests/db_functions/models.py | 1 | ||||
| -rw-r--r-- | tests/db_functions/test_uuid.py | 27 |
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))) |
