diff --git a/django/contrib/postgres/functions.py b/django/contrib/postgres/functions.py index d17f9cb37d..36b32e0751 100644 --- a/django/contrib/postgres/functions.py +++ b/django/contrib/postgres/functions.py @@ -1,4 +1,13 @@ -from django.db.models import DateTimeField, Func +from django.db.models import DateTimeField, Func, UUIDField + + +class RandomUUID(Func): + template = 'GEN_RANDOM_UUID()' + + def __init__(self, output_field=None, **extra): + if output_field is None: + output_field = UUIDField() + super().__init__(output_field=output_field, **extra) class TransactionNow(Func): diff --git a/django/contrib/postgres/operations.py b/django/contrib/postgres/operations.py index 7544e38613..8ff043b5a9 100644 --- a/django/contrib/postgres/operations.py +++ b/django/contrib/postgres/operations.py @@ -35,6 +35,12 @@ class CITextExtension(CreateExtension): self.name = 'citext' +class CryptoExtension(CreateExtension): + + def __init__(self): + self.name = 'pgcrypto' + + class HStoreExtension(CreateExtension): def __init__(self): diff --git a/docs/ref/contrib/postgres/functions.txt b/docs/ref/contrib/postgres/functions.txt index 465d423f6d..8d3df51864 100644 --- a/docs/ref/contrib/postgres/functions.txt +++ b/docs/ref/contrib/postgres/functions.txt @@ -7,6 +7,26 @@ All of these functions are available from the .. currentmodule:: django.contrib.postgres.functions +``RandomUUID`` +============== + +.. class:: RandomUUID() + +.. versionadded:: 2.0 + +Returns a version 4 UUID. + +The `pgcrypto extension`_ must be installed. You can use the +:class:`~django.contrib.postgres.operations.CryptoExtension` migration +operation to install it. + +.. _pgcrypto extension: https://www.postgresql.org/docs/current/static/pgcrypto.html + +Usage example:: + + >>> from django.contrib.postgres.functions import RandomUUID + >>> Article.objects.update(uuid=RandomUUID()) + ``TransactionNow`` ================== diff --git a/docs/ref/contrib/postgres/operations.txt b/docs/ref/contrib/postgres/operations.txt index 90cd007121..d984d9a3f7 100644 --- a/docs/ref/contrib/postgres/operations.txt +++ b/docs/ref/contrib/postgres/operations.txt @@ -67,6 +67,15 @@ run the query ``CREATE EXTENSION IF NOT EXISTS hstore;``. Installs the ``citext`` extension. +``CryptoExtension`` +=================== + +.. class:: CryptoExtension() + + .. versionadded:: 2.0 + + Installs the ``pgcrypto`` extension. + ``HStoreExtension`` =================== diff --git a/docs/releases/2.0.txt b/docs/releases/2.0.txt index 4babd84773..cc1248a9b0 100644 --- a/docs/releases/2.0.txt +++ b/docs/releases/2.0.txt @@ -87,6 +87,12 @@ Minor features :class:`~django.contrib.postgres.aggregates.ArrayAgg` determines if concatenated values will be distinct. +* The new :class:`~django.contrib.postgres.functions.RandomUUID` database + function returns a version 4 UUID. It requires use of PostgreSQL's + ``pgcrypto`` extension which can be activated using the new + :class:`~django.contrib.postgres.operations.CryptoExtension` migration + operation. + :mod:`django.contrib.redirects` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/postgres_tests/migrations/0001_setup_extensions.py b/tests/postgres_tests/migrations/0001_setup_extensions.py index d090ff7fd6..b00c6c0838 100644 --- a/tests/postgres_tests/migrations/0001_setup_extensions.py +++ b/tests/postgres_tests/migrations/0001_setup_extensions.py @@ -4,13 +4,14 @@ from django.db import migrations try: from django.contrib.postgres.operations import ( - BtreeGinExtension, CITextExtension, CreateExtension, HStoreExtension, - TrigramExtension, UnaccentExtension, + BtreeGinExtension, CITextExtension, CreateExtension, CryptoExtension, + HStoreExtension, TrigramExtension, UnaccentExtension, ) except ImportError: BtreeGinExtension = mock.Mock() CITextExtension = mock.Mock() CreateExtension = mock.Mock() + CryptoExtension = mock.Mock() HStoreExtension = mock.Mock() TrigramExtension = mock.Mock() UnaccentExtension = mock.Mock() @@ -24,6 +25,7 @@ class Migration(migrations.Migration): # Ensure CreateExtension quotes extension names by creating one with a # dash in its name. CreateExtension('uuid-ossp'), + CryptoExtension(), HStoreExtension(), TrigramExtension(), UnaccentExtension(), diff --git a/tests/postgres_tests/migrations/0002_create_test_models.py b/tests/postgres_tests/migrations/0002_create_test_models.py index 842848dd47..4cd37e5e43 100644 --- a/tests/postgres_tests/migrations/0002_create_test_models.py +++ b/tests/postgres_tests/migrations/0002_create_test_models.py @@ -191,6 +191,13 @@ class Migration(migrations.Migration): ('when', models.DateTimeField(null=True, default=None)), ] ), + migrations.CreateModel( + name='UUIDTestModel', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('uuid', models.UUIDField(default=None, null=True)), + ] + ), migrations.CreateModel( name='RangesModel', fields=[ diff --git a/tests/postgres_tests/models.py b/tests/postgres_tests/models.py index 15fb5ab4ee..001ed00d0c 100644 --- a/tests/postgres_tests/models.py +++ b/tests/postgres_tests/models.py @@ -171,3 +171,7 @@ class StatTestModel(models.Model): class NowTestModel(models.Model): when = models.DateTimeField(null=True, default=None) + + +class UUIDTestModel(models.Model): + uuid = models.UUIDField(default=None, null=True) diff --git a/tests/postgres_tests/test_functions.py b/tests/postgres_tests/test_functions.py index 620b561325..875a4b9520 100644 --- a/tests/postgres_tests/test_functions.py +++ b/tests/postgres_tests/test_functions.py @@ -1,10 +1,11 @@ +import uuid from datetime import datetime from time import sleep -from django.contrib.postgres.functions import TransactionNow +from django.contrib.postgres.functions import RandomUUID, TransactionNow from . import PostgreSQLTestCase -from .models import NowTestModel +from .models import NowTestModel, UUIDTestModel class TestTransactionNow(PostgreSQLTestCase): @@ -26,3 +27,15 @@ class TestTransactionNow(PostgreSQLTestCase): self.assertIsInstance(m1.when, datetime) self.assertEqual(m1.when, m2.when) + + +class TestRandomUUID(PostgreSQLTestCase): + + def test_random_uuid(self): + m1 = UUIDTestModel.objects.create() + m2 = UUIDTestModel.objects.create() + UUIDTestModel.objects.update(uuid=RandomUUID()) + m1.refresh_from_db() + m2.refresh_from_db() + self.assertIsInstance(m1.uuid, uuid.UUID) + self.assertNotEqual(m1.uuid, m2.uuid)