Add official UUID support, docs and tests, internally using new SQLAlchemy 2.0 types (#992)

*  Add UUID support from sqlalchemy 2.0 update

* ⚰️ Remove dead code for GUID old support

* 📝 Add documentation for UUIDs

* 🧪 Add test for UUIDs field definition and support

* 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks

* ✏️ Fix prerequisites docs for uuid

* ♻️ Update UUID source examples for consistency

Keep consistency with other examples, functions without parameters, and printing info that shows and explains the UUID results (and can also be tested later)

* 📝 Add source examples for selecting UUIDs with session.get()

* 📝 Re-structure UUID docs

* Explain the concepts at the beggining before using them.
* Explain how UUIDs can be used and trusted.
* Explain why UUIDs could be generated on the code, and how they can be used for distributed systems.
* Explain how UUIDs can prevent information leakage.
* Warn about UUIDs storage size.
* Explain that uuid is part of the standard library.
* Explain how default_factory works.
* Explain that creating an instance would generate a new UUID, before it is sent to the DB. This is included and shown in the example, the UUID is printed before saving to the DB.
* Remove sections about other operations that would behave the same as other fields and don't need additional info from what was explained in previous chapters.
* Add two examples to select using UUIDs, similar to the previous ones, mainly to be able to use them in the tests and ensure that it all works, even when SQLite stores the values as strings but the where() or the session.get() receive UUID values (ensure SQLAlchemy does the conversion correctly for SQLite).
* Add an example terminal run of the code, with comments.
* Simplify the ending to keep only the information that wasn't there before, just the "Learn More" with links.

*  Refactor tests with new printed code, extract and check that UUIDs are used in the right places.

*  Add tests for the new extra UUID examples, for session.get()

* 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks

* 📝 Rename variable in example for Python 3.7+ for consistency with 3.10+ (I missed that change before)

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
This commit is contained in:
Esteban Maya
2024-07-16 20:52:03 -05:00
committed by GitHub
parent 3b889e09f7
commit 95936bb508
15 changed files with 890 additions and 49 deletions

View File

@@ -140,5 +140,4 @@ from .sql.expression import select as select
from .sql.expression import tuple_ as tuple_
from .sql.expression import type_coerce as type_coerce
from .sql.expression import within_group as within_group
from .sql.sqltypes import GUID as GUID
from .sql.sqltypes import AutoString as AutoString

View File

@@ -51,7 +51,7 @@ from sqlalchemy.orm.attributes import set_attribute
from sqlalchemy.orm.decl_api import DeclarativeMeta
from sqlalchemy.orm.instrumentation import is_instrumented
from sqlalchemy.sql.schema import MetaData
from sqlalchemy.sql.sqltypes import LargeBinary, Time
from sqlalchemy.sql.sqltypes import LargeBinary, Time, Uuid
from typing_extensions import Literal, deprecated, get_origin
from ._compat import ( # type: ignore[attr-defined]
@@ -80,7 +80,7 @@ from ._compat import ( # type: ignore[attr-defined]
sqlmodel_init,
sqlmodel_validate,
)
from .sql.sqltypes import GUID, AutoString
from .sql.sqltypes import AutoString
if TYPE_CHECKING:
from pydantic._internal._model_construction import ModelMetaclass as ModelMetaclass
@@ -608,7 +608,7 @@ def get_sqlalchemy_type(field: Any) -> Any:
scale=getattr(metadata, "decimal_places", None),
)
if issubclass(type_, uuid.UUID):
return GUID
return Uuid
raise ValueError(f"{type_} has no matching SQLAlchemy type")

View File

@@ -1,10 +1,7 @@
import uuid
from typing import Any, Optional, cast
from typing import Any, cast
from sqlalchemy import CHAR, types
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy import types
from sqlalchemy.engine.interfaces import Dialect
from sqlalchemy.sql.type_api import TypeEngine
class AutoString(types.TypeDecorator): # type: ignore
@@ -17,43 +14,3 @@ class AutoString(types.TypeDecorator): # type: ignore
if impl.length is None and dialect.name == "mysql":
return dialect.type_descriptor(types.String(self.mysql_default_length))
return super().load_dialect_impl(dialect)
# Reference form SQLAlchemy docs: https://docs.sqlalchemy.org/en/14/core/custom_types.html#backend-agnostic-guid-type
# with small modifications
class GUID(types.TypeDecorator): # type: ignore
"""Platform-independent GUID type.
Uses PostgreSQL's UUID type, otherwise uses
CHAR(32), storing as stringified hex values.
"""
impl = CHAR
cache_ok = True
def load_dialect_impl(self, dialect: Dialect) -> TypeEngine[Any]:
if dialect.name == "postgresql":
return dialect.type_descriptor(UUID())
else:
return dialect.type_descriptor(CHAR(32))
def process_bind_param(self, value: Any, dialect: Dialect) -> Optional[str]:
if value is None:
return value
elif dialect.name == "postgresql":
return str(value)
else:
if not isinstance(value, uuid.UUID):
return uuid.UUID(value).hex
else:
# hexstring
return value.hex
def process_result_value(self, value: Any, dialect: Dialect) -> Optional[uuid.UUID]:
if value is None:
return value
else:
if not isinstance(value, uuid.UUID):
value = uuid.UUID(value)
return cast(uuid.UUID, value)