-
Notifications
You must be signed in to change notification settings - Fork 104
1280 UUID v7 support #1284
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
1280 UUID v7 support #1284
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
66ea093
initial prototype
dantownsend f92d1eb
fix docstring
dantownsend 94a0482
fix test
dantownsend 1e7191b
simplify backport
dantownsend 981cb5a
add basic test
dantownsend 5943774
don't run test for cockroachdb
dantownsend 4740f3e
Merge branch 'master' into pr/1284
dantownsend 0c784c7
Merge branch 'master' into pr/1284
dantownsend 2da5ff2
add `UUID7` to api reference
dantownsend 8845550
Merge branch 'master' into pr/1284
dantownsend 7983279
Merge branch 'master' into pr/1284
dantownsend 33d4c9c
load script for older versions of Postgres / CockroachDB
dantownsend faa46ce
add tutorial
dantownsend 63945cd
Update uuid_v7_support.rst
dantownsend 06afb29
add polyfills
dantownsend 04493d5
try fixing cockroachdb test
dantownsend bcab76f
disable uuid7 for cockroachdb for now
dantownsend ce96f7b
update docs
dantownsend 0f00b16
Merge branch 'master' into 1280-uuidv7
dantownsend 871a013
Merge branch 'master' into 1280-uuidv7
dantownsend File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| UUID v7 support | ||
| =============== | ||
|
|
||
| .. note:: CockroachDB does not currently support UUID v7. | ||
|
|
||
| With Postgres 18 and above, UUID v7 is natively supported. | ||
|
|
||
| For this to work in older versions of Postgres, a function has to be registered | ||
| in the database. | ||
|
|
||
|
|
||
| The easiest option is to add the following to :class:`PostgresEngine <piccolo.engines.postgres.PostgresEngine>`: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| DB = PostgresEngine( | ||
| polyfills=['uuid7'], | ||
| ... | ||
| ) | ||
|
|
||
| Which will try and register a ``uuid7`` function on initialisation. | ||
|
|
||
| Alternatives | ||
| ------------ | ||
|
|
||
| If you'd rather, you can register an extension such as `pg_uuidv7 <https://github.com/fboulnois/pg_uuidv7>`_. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,7 @@ | |
| import contextvars | ||
| from collections.abc import Sequence | ||
| from dataclasses import dataclass | ||
| from typing import TYPE_CHECKING, Any, Mapping, Optional, Union | ||
| from typing import TYPE_CHECKING, Any, Literal, Mapping, Optional, Union | ||
|
|
||
| from typing_extensions import Self | ||
|
|
||
|
|
@@ -352,6 +352,7 @@ class PostgresEngine(Engine[PostgresTransaction]): | |
| "config", | ||
| "extensions", | ||
| "extra_nodes", | ||
| "polyfills", | ||
| "pool", | ||
| ) | ||
|
|
||
|
|
@@ -362,12 +363,14 @@ def __init__( | |
| log_queries: bool = False, | ||
| log_responses: bool = False, | ||
| extra_nodes: Optional[Mapping[str, PostgresEngine]] = None, | ||
| polyfills: list[Literal["uuidv7"]] = [], | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is great, really clever. Can you add a description of |
||
| ) -> None: | ||
| if extra_nodes is None: | ||
| extra_nodes = {} | ||
|
|
||
| self.config = config | ||
| self.extensions = extensions | ||
| self.polyfills = polyfills | ||
| self.log_queries = log_queries | ||
| self.log_responses = log_responses | ||
| self.extra_nodes = extra_nodes | ||
|
|
@@ -380,7 +383,7 @@ def __init__( | |
| engine_type="postgres", | ||
| log_queries=log_queries, | ||
| log_responses=log_responses, | ||
| min_version_number=10, | ||
| min_version_number=13, | ||
| ) | ||
|
|
||
| @staticmethod | ||
|
|
@@ -434,6 +437,11 @@ async def prep_database(self): | |
| level=Level.medium, | ||
| ) | ||
|
|
||
| if "uuidv7" in self.polyfills: | ||
| from piccolo.utils.uuid import UUID7_DB_POLYFILL | ||
|
|
||
| await self._run_in_new_connection(UUID7_DB_POLYFILL) | ||
|
|
||
| ########################################################################### | ||
| # These typos existed in the codebase for a while, so leaving these proxy | ||
| # methods for now to ensure backwards compatibility. | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| try: | ||
| from uuid import uuid7 # type: ignore | ||
| except ImportError: | ||
| # For version < Python 3.14 | ||
| from ._uuid_backport import uuid7 | ||
|
|
||
|
|
||
| # https://github.com/dverite/postgres-uuidv7-sql/blob/396a44433e6e0eb63b1d9d1517e9098256d97351/sql/uuidv7-sql--1.0.sql#L6-L19 | ||
| UUID7_DB_POLYFILL = """ | ||
| CREATE OR REPLACE FUNCTION uuidv7( | ||
| timestamptz DEFAULT clock_timestamp() | ||
| ) RETURNS uuid | ||
| AS $$ | ||
| -- Replace the first 48 bits of a uuidv4 with the current | ||
| -- number of milliseconds since 1970-01-01 UTC | ||
| -- and set the "ver" field to 7 by setting additional bits | ||
| select encode( | ||
| set_bit( | ||
| set_bit( | ||
| overlay(uuid_send(gen_random_uuid()) placing | ||
| substring(int8send((extract(epoch from $1)*1000)::bigint) from 3) | ||
| from 1 for 6), | ||
| 52, 1), | ||
| 53, 1), 'hex')::uuid; | ||
| $$ LANGUAGE sql volatile parallel safe; | ||
| """ | ||
|
|
||
|
|
||
| __all__ = ("uuid7", "UUID7_DB_POLYFILL") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| # flake8: noqa | ||
|
|
||
| r""" | ||
| Backport of uuid7 from Python 3.14 - we highly recommend using Python 3.14 if | ||
| using uuid7. This is here as a convenience for testing. | ||
|
|
||
| Original author: Ka-Ping Yee <ping@zesty.ca> | ||
| """ | ||
|
|
||
| import os | ||
| import time | ||
| from uuid import UUID, SafeUUID | ||
|
|
||
| _UINT_128_MAX = (1 << 128) - 1 | ||
| # RFC 4122 variant bits and version bits to activate on a UUID integral value. | ||
| _RFC_4122_VERSION_7_FLAGS = (7 << 76) | (0x8000 << 48) | ||
|
|
||
|
|
||
| _last_timestamp_v7 = None | ||
| _last_counter_v7 = 0 # 42-bit counter | ||
|
|
||
|
|
||
| def _uuid7_get_counter_and_tail(): | ||
| rand = int.from_bytes(os.urandom(10), byteorder="big") | ||
| # 42-bit counter with MSB set to 0 | ||
| counter = (rand >> 32) & 0x1FF_FFFF_FFFF | ||
| # 32-bit random data | ||
| tail = rand & 0xFFFF_FFFF | ||
| return counter, tail | ||
|
|
||
|
|
||
| def uuid7(): | ||
| """Generate a UUID from a Unix timestamp in milliseconds and random bits. | ||
|
|
||
| UUIDv7 objects feature monotonicity within a millisecond. | ||
| """ | ||
| # --- 48 --- -- 4 -- --- 12 --- -- 2 -- --- 30 --- - 32 - | ||
| # unix_ts_ms | version | counter_hi | variant | counter_lo | random | ||
| # | ||
| # 'counter = counter_hi | counter_lo' is a 42-bit counter constructed | ||
| # with Method 1 of RFC 9562, §6.2, and its MSB is set to 0. | ||
| # | ||
| # 'random' is a 32-bit random value regenerated for every new UUID. | ||
| # | ||
| # If multiple UUIDs are generated within the same millisecond, the LSB | ||
| # of 'counter' is incremented by 1. When overflowing, the timestamp is | ||
| # advanced and the counter is reset to a random 42-bit integer with MSB | ||
| # set to 0. | ||
|
|
||
| global _last_timestamp_v7 | ||
| global _last_counter_v7 | ||
|
|
||
| nanoseconds = time.time_ns() | ||
| timestamp_ms = nanoseconds // 1_000_000 | ||
|
|
||
| if _last_timestamp_v7 is None or timestamp_ms > _last_timestamp_v7: | ||
| counter, tail = _uuid7_get_counter_and_tail() | ||
| else: | ||
| if timestamp_ms < _last_timestamp_v7: | ||
| timestamp_ms = _last_timestamp_v7 + 1 | ||
| # advance the 42-bit counter | ||
| counter = _last_counter_v7 + 1 | ||
| if counter > 0x3FF_FFFF_FFFF: | ||
| # advance the 48-bit timestamp | ||
| timestamp_ms += 1 | ||
| counter, tail = _uuid7_get_counter_and_tail() | ||
| else: | ||
| # 32-bit random data | ||
| tail = int.from_bytes(os.urandom(4)) | ||
|
|
||
| unix_ts_ms = timestamp_ms & 0xFFFF_FFFF_FFFF | ||
| counter_msbs = counter >> 30 | ||
| # keep 12 counter's MSBs and clear variant bits | ||
| counter_hi = counter_msbs & 0x0FFF | ||
| # keep 30 counter's LSBs and clear version bits | ||
| counter_lo = counter & 0x3FFF_FFFF | ||
| # ensure that the tail is always a 32-bit integer (by construction, | ||
| # it is already the case, but future interfaces may allow the user | ||
| # to specify the random tail) | ||
| tail &= 0xFFFF_FFFF | ||
|
|
||
| int_uuid_7 = unix_ts_ms << 80 | ||
| int_uuid_7 |= counter_hi << 64 | ||
| int_uuid_7 |= counter_lo << 32 | ||
| int_uuid_7 |= tail | ||
| # by construction, the variant and version bits are already cleared | ||
| int_uuid_7 |= _RFC_4122_VERSION_7_FLAGS | ||
|
|
||
| assert 0 <= int_uuid_7 <= _UINT_128_MAX, repr(int_uuid_7) | ||
| res = UUID(int=int_uuid_7, is_safe=SafeUUID.unknown) | ||
|
|
||
| # defer global update until all computations are done | ||
| _last_timestamp_v7 = timestamp_ms | ||
| _last_counter_v7 = counter | ||
| return res |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,36 @@ | ||
| import uuid | ||
|
|
||
| from piccolo.columns.column_types import UUID | ||
| from piccolo.columns.defaults.uuid import UUID7 | ||
| from piccolo.table import Table | ||
| from piccolo.testing.test_case import TableTest | ||
| from piccolo.testing.test_case import AsyncTableTest | ||
| from tests.base import engines_only | ||
|
|
||
|
|
||
| class MyTable(Table): | ||
| class UUIDTable(Table): | ||
| uuid = UUID() | ||
|
|
||
|
|
||
| class TestUUID(TableTest): | ||
| tables = [MyTable] | ||
| class TestUUID(AsyncTableTest): | ||
| tables = [UUIDTable] | ||
|
|
||
| def test_return_type(self): | ||
| row = MyTable() | ||
| row.save().run_sync() | ||
| async def test_return_type(self): | ||
| row = UUIDTable() | ||
| await row.save() | ||
|
|
||
| self.assertIsInstance(row.uuid, uuid.UUID) | ||
|
|
||
|
|
||
| class UUID7Table(Table): | ||
| uuid_7 = UUID(default=UUID7()) | ||
|
|
||
|
|
||
| @engines_only("postgres", "sqlite") | ||
| class TestUUID7(AsyncTableTest): | ||
| tables = [UUID7Table] | ||
|
|
||
| async def test_return_type(self): | ||
| row = UUID7Table() | ||
| await row.save() | ||
|
|
||
| self.assertIsInstance(row.uuid_7, uuid.UUID) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know if it's a good idea, but since Cockroach doesn't natively support
uuidv7, we can generate default values from Python. Something like thisI tried it locally with Cockroach and it worked. A valid
uuidv7is generated and saved in the database. Piccolo unittests also pass.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally I'd love to find a script which provides UUID7 for Cockroach. I can't get the Postgres one to work with Cockroach:
Even though this works:
We need a function which generates the UUID within the database itself. For now I think I'll just leave out Cockroach support until they natively support it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Never mind, it was just an idea. I haven't found any examples of custom functions that will work in Cockroach, which means it's not that common in Cockroach. You are right that it should be left as it is and wait for the native implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's hard with Cockroach because it's mostly Postgres compatible, but when it comes to functions etc it's missing a few.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm tempted to not even support older versions of Postgres and Python with uuid7, just because it relies on other people's code (the uuid7 Python backport, and the Postgres function), which are under different licenses. It becomes a bit of a mess.
So if people want to use it, they just have to use Python 3.14 and Postgres 18.
We'll just make the tests only run for that version of Python and Postgres.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's actually a good idea and the code will be much simpler. With this we only rely on the
Python 3.14standard library and the nativePostgres 18implementation. That should be emphasized in the documentation.