From 95355a71d5286c4023cbcecbfd3289c76d4ff606 Mon Sep 17 00:00:00 2001 From: Benjamin Skov Kaas-Hansen Date: Thu, 2 Oct 2025 13:45:13 +0200 Subject: [PATCH 1/6] fix URLs Signed-off-by: Benjamin Skov Kaas-Hansen --- AUTHORS.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 05a3b8fc..7ed04256 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -43,10 +43,10 @@ Authors - David Smith - `ddabble `_ - Dmytro Shyshov (`xahgmah `_) -- Edouard Richard (`vied12 ` _) +- Edouard Richard (`vied12 `_) - Eduardo Cuducos - Erik van Widenfelt (`erikvw `_) -- Fábio Capuano (`fabiocapsouza `_) - Filipe Pina (@fopina) - Florian Eßer - François Martin (`martinfrancois `_) From 73f4f1a13024254df573153117d8367b1351cd07 Mon Sep 17 00:00:00 2001 From: Benjamin Skov Kaas-Hansen Date: Thu, 2 Oct 2025 14:06:32 +0200 Subject: [PATCH 2/6] add settings flag to allow using UUIDv7 Signed-off-by: Benjamin Skov Kaas-Hansen --- simple_history/models.py | 13 ++++++++++++- simple_history/utils.py | 26 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/simple_history/models.py b/simple_history/models.py index f38987cc..b3b7b78d 100644 --- a/simple_history/models.py +++ b/simple_history/models.py @@ -429,8 +429,19 @@ def _get_history_id_field(self): history_id_field.primary_key = True history_id_field.editable = False elif getattr(settings, "SIMPLE_HISTORY_HISTORY_ID_USE_UUID", False): + uuid_version = getattr( + settings, "SIMPLE_HISTORY_HISTORY_ID_UUID_VERSION", 4 + ) + if uuid_version == 4: + uuid_default = uuid.uuid4 + elif uuid_version == 7: + uuid_default = utils.uuid7 + else: + raise ImproperlyConfigured( + "SIMPLE_HISTORY_HISTORY_ID_UUID_VERSION must be either 4 or 7" + ) history_id_field = models.UUIDField( - primary_key=True, default=uuid.uuid4, editable=False + primary_key=True, default=uuid_default, editable=False ) else: history_id_field = models.AutoField(primary_key=True) diff --git a/simple_history/utils.py b/simple_history/utils.py index 0fdfc629..b02c51bf 100644 --- a/simple_history/utils.py +++ b/simple_history/utils.py @@ -1,3 +1,7 @@ +import os +import time +import uuid + from django.db import transaction from django.db.models import Case, ForeignKey, ManyToManyField, Q, When from django.forms.models import model_to_dict @@ -241,3 +245,25 @@ def get_change_reason_from_object(obj): return getattr(obj, "_change_reason") return None + + +def uuid7(): + """ + Custom function to generate a UUIDv7 (time-based UUID) as per + https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-7. + NB! Although uuid.UUID() has a version parameter, we cannot set it to 7 + as of 2025-06-07 because UUID7 isn't part of the module--it it were, + this function would be redundant. + """ + + # Initialise random bytearray + res = bytearray(os.urandom(16)) + + # Replace first 6 bytes (= 48 bits) with timestamp values + res[0:6] = (time.time_ns() // 1_000_000).to_bytes(6, "big") + + # Then, set version and variant bits + res[6] = (res[6] & 0x0F) | 0x70 + res[8] = (res[8] & 0x3F) | 0x80 + + return uuid.UUID(bytes=bytes(res)) From a3a51ce04ac5c1f8444fe0ae8d48241ed7ab903c Mon Sep 17 00:00:00 2001 From: Benjamin Skov Kaas-Hansen Date: Thu, 2 Oct 2025 14:06:48 +0200 Subject: [PATCH 3/6] add tests for UUIDv7 history_id Signed-off-by: Benjamin Skov Kaas-Hansen --- simple_history/tests/models.py | 12 ++++++++++++ simple_history/tests/tests/test_models.py | 17 +++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/simple_history/tests/models.py b/simple_history/tests/models.py index c6771302..62e92c1b 100644 --- a/simple_history/tests/models.py +++ b/simple_history/tests/models.py @@ -797,6 +797,18 @@ class UUIDDefaultModel(models.Model): history = HistoricalRecords() +# Set the SIMPLE_HISTORY_HISTORY_ID_UUID_VERSION +setattr(settings, "SIMPLE_HISTORY_HISTORY_ID_UUID_VERSION", 7) + + +class UUIDv7Model(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + history = HistoricalRecords() + + +# Clear the SIMPLE_HISTORY_HISTORY_ID_UUID_VERSION +delattr(settings, "SIMPLE_HISTORY_HISTORY_ID_UUID_VERSION") + # Clear the SIMPLE_HISTORY_HISTORY_ID_USE_UUID delattr(settings, "SIMPLE_HISTORY_HISTORY_ID_USE_UUID") diff --git a/simple_history/tests/tests/test_models.py b/simple_history/tests/tests/test_models.py index 2a1c08ec..1dbab30b 100644 --- a/simple_history/tests/tests/test_models.py +++ b/simple_history/tests/tests/test_models.py @@ -1,4 +1,5 @@ import dataclasses +import time import unittest import uuid import warnings @@ -122,6 +123,7 @@ UserTextFieldChangeReasonModel, UUIDDefaultModel, UUIDModel, + UUIDv7Model, WaterLevel, ) from .utils import ( @@ -637,6 +639,21 @@ def test_uuid_default_history_id(self): history = entry.history.all()[0] self.assertTrue(isinstance(history.history_id, uuid.UUID)) + def test_uuidv7_history_id(self): + """ + Tests that UUIDv7 is being used for history_id when defined in the settings, + and that consequent PKs are ordered based on creation time. + """ + entries = [] + for _ in range(10): + entries.append(UUIDv7Model.objects.create()) + time.sleep(0.01) # Ensure increasing time part of UUIDv7 PKs + + history = [entry.history.all()[0] for entry in entries] + self.assertTrue(all(isinstance(h.history_id, uuid.UUID) for h in history)) + for h1, h2 in zip(history[:-1], history[1:]): + self.assertLess(h1.history_id, h2.history_id) + def test_default_history_change_reason(self): entry = CharFieldChangeReasonModel.objects.create(greeting="what's up?") history = entry.history.get() From f73d1505787fe5cb7a85e9939d6e86295b1f22a8 Mon Sep 17 00:00:00 2001 From: Benjamin Skov Kaas-Hansen Date: Thu, 2 Oct 2025 14:07:05 +0200 Subject: [PATCH 4/6] update documentation Signed-off-by: Benjamin Skov Kaas-Hansen --- docs/historical_model.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/historical_model.rst b/docs/historical_model.rst index 32447f18..78c4df45 100644 --- a/docs/historical_model.rst +++ b/docs/historical_model.rst @@ -27,13 +27,15 @@ The example below uses a ``UUIDField`` instead of an ``AutoField``: Since using a ``UUIDField`` for the ``history_id`` is a common use case, there is a ``SIMPLE_HISTORY_HISTORY_ID_USE_UUID`` setting that will set all instances of ``history_id`` to UUIDs. +By default, this will use UUIDv4, but you can choose to use UUIDv7 by setting +``SIMPLE_HISTORY_HISTORY_ID_UUID_VERSION`` to ``7``. Set this with the following line in your ``settings.py`` file: .. code-block:: python SIMPLE_HISTORY_HISTORY_ID_USE_UUID = True - + SIMPLE_HISTORY_HISTORY_ID_UUID_VERSION = 7 # Optional This setting can still be overridden using the ``history_id_field`` parameter on a per model basis. From 8e40131dbea10c7db85e839cfdfa0171d169d454 Mon Sep 17 00:00:00 2001 From: Benjamin Skov Kaas-Hansen Date: Thu, 2 Oct 2025 14:07:15 +0200 Subject: [PATCH 5/6] update AUTHORS Signed-off-by: Benjamin Skov Kaas-Hansen --- AUTHORS.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 7ed04256..e83a022d 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -19,6 +19,7 @@ Authors - Anton Kulikov (`bigtimecriminal `_) - Ben Lawson (`blawson `_) - Benjamin Mampaey (`bmampaey `_) +- Benjamin Skov Kaas-Hansen (`epiben `_) - Berke Agababaoglu (`bagababaoglu `_) - Bheesham Persaud (`bheesham `_) - `bradford281 `_ From 20f29e6ea0be70d32a9f96f0e0348376723e5627 Mon Sep 17 00:00:00 2001 From: Benjamin Skov Kaas-Hansen Date: Thu, 2 Oct 2025 14:14:53 +0200 Subject: [PATCH 6/6] update CHANGES Signed-off-by: Benjamin Skov Kaas-Hansen --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index 34a86b8c..748f164d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,7 @@ Unreleased - Added support for Python 3.14 - Added support for Django 6.0 +- Added UUIDv7 support for ``history_id`` fields (gh-1532) 3.10.1 (2025-06-20) -------------------