Skip to content

Commit 47ee1ed

Browse files
authored
feat(utils): add UUID and ID generation utilities (#293)
Add sqlspec.utils.uuids module providing wrapper functions for `uuid3`, `uuid4`, `uuid5`, `uuid6`, `uuid7`, and `nanoid` generation. Uses `uuid-utils` and `fastnanoid` packages for performance when available, falling back gracefully to stdlib with appropriate warnings.
1 parent daca7b2 commit 47ee1ed

File tree

4 files changed

+720
-0
lines changed

4 files changed

+720
-0
lines changed

sqlspec/_typing.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,8 @@ async def insert_returning(self, conn: Any, query_name: str, sql: str, parameter
697697
PYARROW_INSTALLED = dependency_flag("pyarrow")
698698
PYDANTIC_INSTALLED = dependency_flag("pydantic")
699699
ALLOYDB_CONNECTOR_INSTALLED = dependency_flag("google.cloud.alloydb.connector")
700+
NANOID_INSTALLED = dependency_flag("fastnanoid")
701+
UUID_UTILS_INSTALLED = dependency_flag("uuid_utils")
700702

701703
__all__ = (
702704
"AIOSQL_INSTALLED",
@@ -707,6 +709,7 @@ async def insert_returning(self, conn: Any, query_name: str, sql: str, parameter
707709
"FSSPEC_INSTALLED",
708710
"LITESTAR_INSTALLED",
709711
"MSGSPEC_INSTALLED",
712+
"NANOID_INSTALLED",
710713
"NUMPY_INSTALLED",
711714
"OBSTORE_INSTALLED",
712715
"OPENTELEMETRY_INSTALLED",
@@ -719,6 +722,7 @@ async def insert_returning(self, conn: Any, query_name: str, sql: str, parameter
719722
"PYDANTIC_INSTALLED",
720723
"UNSET",
721724
"UNSET_STUB",
725+
"UUID_UTILS_INSTALLED",
722726
"AiosqlAsyncProtocol",
723727
"AiosqlParamType",
724728
"AiosqlSQLOperationType",

sqlspec/typing.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
FSSPEC_INSTALLED,
1515
LITESTAR_INSTALLED,
1616
MSGSPEC_INSTALLED,
17+
NANOID_INSTALLED,
1718
NUMPY_INSTALLED,
1819
OBSTORE_INSTALLED,
1920
OPENTELEMETRY_INSTALLED,
@@ -25,6 +26,7 @@
2526
PYARROW_INSTALLED,
2627
PYDANTIC_INSTALLED,
2728
UNSET,
29+
UUID_UTILS_INSTALLED,
2830
AiosqlAsyncProtocol,
2931
AiosqlParamType,
3032
AiosqlSQLOperationType,
@@ -154,6 +156,7 @@ def get_type_adapter(f: "type[T]") -> Any:
154156
"FSSPEC_INSTALLED",
155157
"LITESTAR_INSTALLED",
156158
"MSGSPEC_INSTALLED",
159+
"NANOID_INSTALLED",
157160
"NUMPY_INSTALLED",
158161
"OBSTORE_INSTALLED",
159162
"OPENTELEMETRY_INSTALLED",
@@ -166,6 +169,7 @@ def get_type_adapter(f: "type[T]") -> Any:
166169
"PYDANTIC_INSTALLED",
167170
"PYDANTIC_USE_FAILFAST",
168171
"UNSET",
172+
"UUID_UTILS_INSTALLED",
169173
"AiosqlAsyncProtocol",
170174
"AiosqlParamType",
171175
"AiosqlSQLOperationType",

sqlspec/utils/uuids.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
"""UUID and ID generation utilities with optional acceleration.
2+
3+
Provides wrapper functions for uuid3, uuid4, uuid5, uuid6, uuid7, and nanoid generation.
4+
Uses uuid-utils and fastnanoid packages for performance when available,
5+
falling back to standard library.
6+
7+
When uuid-utils is installed:
8+
- uuid3, uuid4, uuid5, uuid6, uuid7 use the faster Rust implementation
9+
- uuid6 and uuid7 provide proper time-ordered UUIDs per RFC 9562
10+
11+
When uuid-utils is NOT installed:
12+
- uuid3, uuid4, uuid5 fall back silently to stdlib (equivalent output)
13+
- uuid6, uuid7 fall back to uuid4 with a one-time warning (different UUID version)
14+
15+
When fastnanoid is installed:
16+
- nanoid() uses the Rust implementation for 21-char URL-safe IDs
17+
18+
When fastnanoid is NOT installed:
19+
- nanoid() falls back to uuid4().hex with a one-time warning (different format)
20+
"""
21+
22+
import warnings
23+
from typing import TYPE_CHECKING, Any
24+
from uuid import UUID
25+
from uuid import uuid3 as _stdlib_uuid3
26+
from uuid import uuid4 as _stdlib_uuid4
27+
from uuid import uuid5 as _stdlib_uuid5
28+
29+
from sqlspec.typing import NANOID_INSTALLED, UUID_UTILS_INSTALLED
30+
31+
__all__ = (
32+
"NAMESPACE_DNS",
33+
"NAMESPACE_OID",
34+
"NAMESPACE_URL",
35+
"NAMESPACE_X500",
36+
"NANOID_INSTALLED",
37+
"UUID_UTILS_INSTALLED",
38+
"nanoid",
39+
"uuid3",
40+
"uuid4",
41+
"uuid5",
42+
"uuid6",
43+
"uuid7",
44+
)
45+
46+
_uuid6_warned: bool = False
47+
_uuid7_warned: bool = False
48+
_nanoid_warned: bool = False
49+
50+
if UUID_UTILS_INSTALLED and not TYPE_CHECKING:
51+
from uuid_utils import NAMESPACE_DNS, NAMESPACE_OID, NAMESPACE_URL, NAMESPACE_X500
52+
from uuid_utils import UUID as _UUID_UTILS_UUID
53+
from uuid_utils import uuid3 as _uuid3
54+
from uuid_utils import uuid4 as _uuid4
55+
from uuid_utils import uuid5 as _uuid5
56+
from uuid_utils import uuid6 as _uuid6
57+
from uuid_utils import uuid7 as _uuid7
58+
59+
def _convert_namespace(namespace: "Any") -> "_UUID_UTILS_UUID":
60+
"""Convert a namespace to uuid_utils.UUID if needed."""
61+
if isinstance(namespace, _UUID_UTILS_UUID):
62+
return namespace
63+
return _UUID_UTILS_UUID(str(namespace))
64+
65+
else:
66+
from uuid import NAMESPACE_DNS, NAMESPACE_OID, NAMESPACE_URL, NAMESPACE_X500
67+
68+
_uuid3 = _stdlib_uuid3
69+
_uuid4 = _stdlib_uuid4
70+
_uuid5 = _stdlib_uuid5
71+
_uuid6 = _stdlib_uuid4
72+
_uuid7 = _stdlib_uuid4
73+
_UUID_UTILS_UUID = UUID
74+
75+
def _convert_namespace(namespace: "Any") -> "UUID":
76+
"""Pass through namespace when uuid-utils is not installed."""
77+
return namespace # type: ignore[no-any-return]
78+
79+
80+
if NANOID_INSTALLED and not TYPE_CHECKING:
81+
from fastnanoid import generate as _nanoid_impl
82+
else:
83+
84+
def _nanoid_impl() -> str:
85+
return _stdlib_uuid4().hex
86+
87+
88+
def uuid3(name: str, namespace: "UUID | None" = None) -> "UUID":
89+
"""Generate a deterministic UUID (version 3) using MD5 hash.
90+
91+
Uses uuid-utils for performance when available, falls back to
92+
standard library uuid.uuid3() silently (equivalent output).
93+
94+
Args:
95+
name: The name to hash within the namespace.
96+
namespace: The namespace UUID. Defaults to NAMESPACE_DNS if not provided.
97+
98+
Returns:
99+
A deterministic UUID based on namespace and name.
100+
"""
101+
namespace = NAMESPACE_DNS if namespace is None else _convert_namespace(namespace)
102+
return _uuid3(namespace, name)
103+
104+
105+
def uuid4() -> "UUID":
106+
"""Generate a random UUID (version 4).
107+
108+
Uses uuid-utils for performance when available, falls back to
109+
standard library uuid.uuid4() silently (equivalent output).
110+
111+
Returns:
112+
A randomly generated UUID.
113+
"""
114+
return _uuid4()
115+
116+
117+
def uuid5(name: str, namespace: "UUID | None" = None) -> "UUID":
118+
"""Generate a deterministic UUID (version 5) using SHA-1 hash.
119+
120+
Uses uuid-utils for performance when available, falls back to
121+
standard library uuid.uuid5() silently (equivalent output).
122+
123+
Args:
124+
name: The name to hash within the namespace.
125+
namespace: The namespace UUID. Defaults to NAMESPACE_DNS if not provided.
126+
127+
Returns:
128+
A deterministic UUID based on namespace and name.
129+
"""
130+
namespace = NAMESPACE_DNS if namespace is None else _convert_namespace(namespace)
131+
return _uuid5(namespace, name)
132+
133+
134+
def uuid6() -> "UUID":
135+
"""Generate a time-ordered UUID (version 6).
136+
137+
Uses uuid-utils when available. When uuid-utils is not installed,
138+
falls back to uuid4() with a warning (emitted once per session).
139+
140+
UUIDv6 is lexicographically sortable by timestamp, making it
141+
suitable for database primary keys. It is a reordering of UUIDv1
142+
fields to improve database performance.
143+
144+
Returns:
145+
A time-ordered UUID, or a random UUID if uuid-utils unavailable.
146+
"""
147+
global _uuid6_warned
148+
if not UUID_UTILS_INSTALLED and not _uuid6_warned:
149+
warnings.warn(
150+
"uuid-utils not installed, falling back to uuid4 for UUID v6 generation. "
151+
"Install with: pip install sqlspec[uuid]",
152+
UserWarning,
153+
stacklevel=2,
154+
)
155+
_uuid6_warned = True
156+
return _uuid6()
157+
158+
159+
def uuid7() -> "UUID":
160+
"""Generate a time-ordered UUID (version 7).
161+
162+
Uses uuid-utils when available. When uuid-utils is not installed,
163+
falls back to uuid4() with a warning (emitted once per session).
164+
165+
UUIDv7 is the recommended time-ordered UUID format per RFC 9562,
166+
providing millisecond precision timestamps. It is designed for
167+
modern distributed systems and database primary keys.
168+
169+
Returns:
170+
A time-ordered UUID, or a random UUID if uuid-utils unavailable.
171+
"""
172+
global _uuid7_warned
173+
if not UUID_UTILS_INSTALLED and not _uuid7_warned:
174+
warnings.warn(
175+
"uuid-utils not installed, falling back to uuid4 for UUID v7 generation. "
176+
"Install with: pip install sqlspec[uuid]",
177+
UserWarning,
178+
stacklevel=2,
179+
)
180+
_uuid7_warned = True
181+
return _uuid7()
182+
183+
184+
def nanoid() -> str:
185+
"""Generate a Nano ID.
186+
187+
Uses fastnanoid for performance when available. When fastnanoid is
188+
not installed, falls back to uuid4().hex with a warning (emitted
189+
once per session).
190+
191+
Nano IDs are URL-safe, compact 21-character identifiers suitable
192+
for use as primary keys or short identifiers. The default alphabet
193+
uses A-Za-z0-9_- characters.
194+
195+
Returns:
196+
A 21-character Nano ID string, or 32-character UUID hex if
197+
fastnanoid unavailable.
198+
"""
199+
global _nanoid_warned
200+
if not NANOID_INSTALLED and not _nanoid_warned:
201+
warnings.warn(
202+
"fastnanoid not installed, falling back to uuid4.hex for Nano ID generation. "
203+
"Install with: pip install sqlspec[nanoid]",
204+
UserWarning,
205+
stacklevel=2,
206+
)
207+
_nanoid_warned = True
208+
return _nanoid_impl()

0 commit comments

Comments
 (0)