|
24 | 24 | import threading |
25 | 25 | from django.db import transaction |
26 | 26 | # from simple_history.utils import update_change_reason |
27 | | -import time |
28 | | -import os |
29 | | - |
| 27 | +from django.db.models import Func, UUIDField |
| 28 | +from django.db import db_connection |
| 29 | +from uuid6 import uuid7 as uuidv7 # noqa: F401 |
30 | 30 | try: |
31 | 31 | from simple_history.models import HistoricalRecords |
32 | 32 | except Exception: |
33 | 33 | HistoricalRecords = None |
34 | 34 |
|
35 | | - |
36 | 35 | _request_local = threading.local() |
37 | 36 |
|
38 | 37 | logger = logging.getLogger(__file__) |
@@ -603,29 +602,72 @@ def clean_fk(instance): |
603 | 602 | return field_values |
604 | 603 |
|
605 | 604 |
|
606 | | -def uuidv7() -> uuid.UUID: |
| 605 | +class GenerateUUIDv7(Func): |
607 | 606 | """ |
608 | | - Generate a UUIDv7. |
609 | | - """ |
610 | | - # random bytes |
611 | | - value = bytearray(os.urandom(16)) |
| 607 | + Cross-database UUIDv7 generator. |
612 | 608 |
|
613 | | - # current timestamp in ms |
614 | | - timestamp = int(time.time() * 1000) |
| 609 | + Usage: |
| 610 | + from yourapp.db_functions import GenerateUUIDv7 |
615 | 611 |
|
616 | | - # timestamp |
617 | | - value[0] = (timestamp >> 40) & 0xFF |
618 | | - value[1] = (timestamp >> 32) & 0xFF |
619 | | - value[2] = (timestamp >> 24) & 0xFF |
620 | | - value[3] = (timestamp >> 16) & 0xFF |
621 | | - value[4] = (timestamp >> 8) & 0xFF |
622 | | - value[5] = timestamp & 0xFF |
| 612 | + class YourModel(models.Model): |
| 613 | + id = models.UUIDField( |
| 614 | + primary_key=True, |
| 615 | + db_default=GenerateUUIDv7(), |
| 616 | + editable=False, |
| 617 | + ) |
| 618 | + """ |
| 619 | + output_field = UUIDField() |
| 620 | + template = '%(function)s()' |
| 621 | + |
| 622 | + def as_sql(self, compiler, connection, **extra_context): |
| 623 | + vendor = db_connection.vendor |
| 624 | + |
| 625 | + if vendor == 'postgresql': |
| 626 | + # PostgreSQL 18+ has native uuidv7() |
| 627 | + # Older versions use our custom uuid_generate_v7() |
| 628 | + pg_version = getattr(connection, 'pg_version', 0) |
| 629 | + if pg_version >= 180000: |
| 630 | + function = 'uuidv7' |
| 631 | + else: |
| 632 | + function = 'uuid_generate_v7' |
623 | 633 |
|
624 | | - # version and variant |
625 | | - value[6] = (value[6] & 0x0F) | 0x70 |
626 | | - value[8] = (value[8] & 0x3F) | 0x80 |
| 634 | + elif vendor == 'microsoft': |
| 635 | + # SQL Server - uses our custom function |
| 636 | + # Change to 'dbo.uuid_v8mssql' if you want the optimized version |
| 637 | + # for better clustered index performance |
| 638 | + function = 'dbo.uuid_v7' |
627 | 639 |
|
628 | | - return uuid.UUID(bytes=bytes(value)) |
| 640 | + else: |
| 641 | + # Fallback for other databases (or raise error) |
| 642 | + function = 'uuid_generate_v7' |
| 643 | + |
| 644 | + extra_context['function'] = function |
| 645 | + return super().as_sql(compiler, connection, **extra_context) |
| 646 | + |
| 647 | + |
| 648 | +class RandomUUID(Func): |
| 649 | + """Cross-database random UUID for Django 4.2""" |
| 650 | + function = None # Will be overridden per backend |
| 651 | + output_field = UUIDField() |
| 652 | + arity = 0 |
| 653 | + |
| 654 | + def as_postgresql(self, compiler, connection, **extra_context): |
| 655 | + # PostgreSQL 13+ has gen_random_uuid() built-in |
| 656 | + return self.as_sql(compiler, connection, function='gen_random_uuid', **extra_context) |
| 657 | + |
| 658 | + def as_microsoft(self, compiler, connection, **extra_context): # MSSQL |
| 659 | + return self.as_sql(compiler, connection, function='NEWID', **extra_context) |
| 660 | + |
| 661 | + # Optional: fallback for other DBs (e.g. SQLite for dev) |
| 662 | + def as_sql(self, compiler, connection, **extra_context): |
| 663 | + # if connection.vendor == 'postgresql': |
| 664 | + # return self.as_postgresql(compiler, connection, **extra_context) |
| 665 | + # elif connection.vendor in ('microsoft', 'mssql'): |
| 666 | + # return self.as_microsoft(compiler, connection, **extra_context) |
| 667 | + if extra_context.get('function', self.function) is None: |
| 668 | + # You can raise or use a default |
| 669 | + raise NotImplementedError(f"RandomUUID not supported on {connection.vendor}") |
| 670 | + return super().as_sql(compiler, connection, **extra_context) |
629 | 671 |
|
630 | 672 |
|
631 | 673 | class CachedModelMixin: |
|
0 commit comments