diff --git a/docs/advanced/uuid.md b/docs/advanced/uuid.md new file mode 100644 index 0000000000..56492636f6 --- /dev/null +++ b/docs/advanced/uuid.md @@ -0,0 +1,342 @@ +# UUID (Universally Unique Identifiers) + +We have discussed some data types like `str`, `int`, etc. + +There's another data type called `UUID` (Universally Unique Identifier). + +You might have seen **UUIDs**, for example in URLs. They look something like this: + +``` +4ff2dab7-bffe-414d-88a5-1826b9fea8df +``` + +UUIDs can be particularly useful as an alternative to auto-incrementing integers for **primary keys**. + +/// info + +Official support for UUIDs was added in SQLModel version `0.0.20`. + +/// + +## About UUIDs + +UUIDs are numbers with 128 bits, that is, 16 bytes. + +They are normally seen as 32 hexadecimal characters separated by dashes. + +There are several versions of UUID, some versions include the current time in the bytes, but **UUIDs version 4** are mainly random, the way they are generated makes them virtually **unique**. + +### Distributed UUIDs + +You could generate one UUID in one computer, and someone else could generate another UUID in another computer, and it would be almost **impossible** for both UUIDs to be the **same**. + +This means that you don't have to wait for the DB to generate the ID for you, you can **generate it in code before sending it to the database**, because you can be quite certain it will be unique. + +/// note | Technical Details + +Because the number of possible UUIDs is so large (2^128), the probability of generating the same UUID version 4 (the random ones) twice is very low. + +If you had 103 trillion version 4 UUIDs stored in the database, the probability of generating a duplicated new one is one in a billion. 🤓 + +/// + +For the same reason, if you decided to migrate your database, combine it with another database and mix records, etc. you would most probably be able to **just use the same UUIDs** you had originally. + +/// warning + +There's still a chance you could have a collision, but it's very low. In most cases you could assume you wouldn't have it, but it would be good to be prepared for it. + +/// + +### UUIDs Prevent Information Leakage + +Because UUIDs version 4 are **random**, you could give these IDs to the application users or to other systems, **without exposing information** about your application. + +When using **auto-incremented integers** for primary keys, you could implicitly expose information about your system. For example, someone could create a new hero, and by getting the hero ID `20` **they would know that you have 20 heroes** in your system (or even less, if some heroes were already deleted). + +### UUID Storage + +Because UUIDs are 16 bytes, they would **consume more space** in the database than a smaller auto-incremented integer (commonly 4 bytes). + +Depending on the database you use, UUIDs could have **better or worse performance**. If you are concerned about that, you should check the documentation for the specific database. + +SQLite doesn't have a specific UUID type, so it will store the UUID as a string. Other databases like Postgres have a specific UUID type which would result in better performance and space usage than strings. + +## Models with UUIDs + +To use UUIDs as primary keys we need to import `uuid`, which is part of the Python standard library (we don't have to install anything) and use `uuid.UUID` as the **type** for the ID field. + +We also want the Python code to **generate a new UUID** when creating a new instance, so we use `default_factory`. + +The parameter `default_factory` takes a function (or in general, a "callable"). This function will be **called when creating a new instance** of the model and the value returned by the function will be used as the default value for the field. + +For the function in `default_factory` we pass `uuid.uuid4`, which is a function that generates a **new UUID version 4**. + +/// tip + +We don't call `uuid.uuid4()` ourselves in the code (we don't put the parenthesis). Instead, we pass the function itself, just `uuid.uuid4`, so that SQLModel can call it every time we create a new instance. + +/// + +This means that the UUID will be generated in the Python code, **before sending the data to the database**. + +//// tab | Python 3.10+ + +```Python hl_lines="1 7" +{!./docs_src/advanced/uuid/tutorial001_py310.py[ln:1-10]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + +```Python hl_lines="1 8" +{!./docs_src/advanced/uuid/tutorial001.py[ln:1-11]!} + +# Code below omitted 👇 +``` + +//// + +/// details | 👀 Full file preview + +//// tab | Python 3.10+ + +```Python +{!./docs_src/advanced/uuid/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + +```Python +{!./docs_src/advanced/uuid/tutorial001.py!} +``` + +//// + +/// + +Pydantic has support for `UUID` types. + +For the database, **SQLModel** internally uses SQLAlchemy's `Uuid` type. + +### Create a Record with a UUID + +When creating a `Hero` record, the `id` field will be **automatically populated** with a new UUID because we set `default_factory=uuid.uuid4`. + +As `uuid.uuid4` will be called when creating the model instance, even before sending it to the database, we can **access and use the ID right away**. + +And that **same ID (a UUID)** will be saved in the database. + +//// tab | Python 3.10+ + +```Python hl_lines="5 7 9 14" +# Code above omitted 👆 + +{!./docs_src/advanced/uuid/tutorial001_py310.py[ln:23-34]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + +```Python hl_lines="5 7 9 14" +# Code above omitted 👆 + +{!./docs_src/advanced/uuid/tutorial001.py[ln:24-35]!} + +# Code below omitted 👇 +``` + +//// + +/// details | 👀 Full file preview + +//// tab | Python 3.10+ + +```Python +{!./docs_src/advanced/uuid/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + +```Python +{!./docs_src/advanced/uuid/tutorial001.py!} +``` + +//// + +/// + +### Select a Hero + +We can do the same operations we could do with other fields. + +For example we can **select a hero by ID**: + +//// tab | Python 3.10+ + +```Python hl_lines="15" +# Code above omitted 👆 + +{!./docs_src/advanced/uuid/tutorial001_py310.py[ln:37-54]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + +```Python hl_lines="15" +# Code above omitted 👆 + +{!./docs_src/advanced/uuid/tutorial001.py[ln:38-55]!} + +# Code below omitted 👇 +``` + +//// + +/// details | 👀 Full file preview + +//// tab | Python 3.10+ + +```Python +{!./docs_src/advanced/uuid/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + +```Python +{!./docs_src/advanced/uuid/tutorial001.py!} +``` + +//// + +/// + +/// tip + +Even if a database like SQLite stores the UUID as a string, we can select and run comparisons using a Python UUID object and it will work. + +SQLModel (actually SQLAlchemy) will take care of making it work. ✨ + +/// + +#### Select with `session.get()` + +We could also select by ID with `session.get()`: + +//// tab | Python 3.10+ + +```Python hl_lines="15" +# Code above omitted 👆 + +{!./docs_src/advanced/uuid/tutorial002_py310.py[ln:37-54]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + +```Python hl_lines="15" +# Code above omitted 👆 + +{!./docs_src/advanced/uuid/tutorial002.py[ln:38-55]!} + +# Code below omitted 👇 +``` + +//// + +/// details | 👀 Full file preview + +//// tab | Python 3.10+ + +```Python +{!./docs_src/advanced/uuid/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + +```Python +{!./docs_src/advanced/uuid/tutorial002.py!} +``` + +//// + +/// + +The same way as with other fields, we could update, delete, etc. 🚀 + +### Run the program + +If you run the program, you will see the **UUID** generated in the Python code, and then the record **saved in the database with the same UUID**. + +
+ +```console +$ python app.py + +// Some boilerplate and previous output omitted 😉 + +// In SQLite, the UUID will be stored as a string +// other DBs like Postgres have a specific UUID type +CREATE TABLE hero ( + id CHAR(32) NOT NULL, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, + PRIMARY KEY (id) +) + +// Before saving in the DB we already have the UUID +The hero before saving in the DB +name='Deadpond' secret_name='Dive Wilson' id=UUID('0e44c1a6-88d3-4a35-8b8a-307faa2def28') age=None +The hero ID was already set +0e44c1a6-88d3-4a35-8b8a-307faa2def28 + +// The SQL statement to insert the record uses our UUID +INSERT INTO hero (id, name, secret_name, age) VALUES (?, ?, ?, ?) +('0e44c1a688d34a358b8a307faa2def28', 'Deadpond', 'Dive Wilson', None) + +// And indeed, the record was saved with the UUID we created 😎 +After saving in the DB +age=None id=UUID('0e44c1a6-88d3-4a35-8b8a-307faa2def28') name='Deadpond' secret_name='Dive Wilson' + +// Now we create a new hero (to select it in a bit) +Created hero: +age=None id=UUID('9d90d186-85db-4eaa-891a-def7b4ae2dab') name='Spider-Boy' secret_name='Pedro Parqueador' +Created hero ID: +9d90d186-85db-4eaa-891a-def7b4ae2dab + +// And now we select it +Selected hero: +age=None id=UUID('9d90d186-85db-4eaa-891a-def7b4ae2dab') name='Spider-Boy' secret_name='Pedro Parqueador' +Selected hero ID: +9d90d186-85db-4eaa-891a-def7b4ae2dab +``` + +
+ +## Learn More + +You can learn more about **UUIDs** in: + +* The official Python docs for UUID. +* The Wikipedia for UUID. diff --git a/docs_src/advanced/uuid/__init__.py b/docs_src/advanced/uuid/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/advanced/uuid/tutorial001.py b/docs_src/advanced/uuid/tutorial001.py new file mode 100644 index 0000000000..cfd3146b41 --- /dev/null +++ b/docs_src/advanced/uuid/tutorial001.py @@ -0,0 +1,65 @@ +import uuid +from typing import Union + +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Union[int, None] = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_hero(): + with Session(engine) as session: + hero = Hero(name="Deadpond", secret_name="Dive Wilson") + print("The hero before saving in the DB") + print(hero) + print("The hero ID was already set") + print(hero.id) + session.add(hero) + session.commit() + session.refresh(hero) + print("After saving in the DB") + print(hero) + + +def select_hero(): + with Session(engine) as session: + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_2) + session.commit() + session.refresh(hero_2) + hero_id = hero_2.id + print("Created hero:") + print(hero_2) + print("Created hero ID:") + print(hero_id) + + statement = select(Hero).where(Hero.id == hero_id) + selected_hero = session.exec(statement).one() + print("Selected hero:") + print(selected_hero) + print("Selected hero ID:") + print(selected_hero.id) + + +def main() -> None: + create_db_and_tables() + create_hero() + select_hero() + + +if __name__ == "__main__": + main() diff --git a/docs_src/advanced/uuid/tutorial001_py310.py b/docs_src/advanced/uuid/tutorial001_py310.py new file mode 100644 index 0000000000..610ec6b0d4 --- /dev/null +++ b/docs_src/advanced/uuid/tutorial001_py310.py @@ -0,0 +1,64 @@ +import uuid + +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_hero(): + with Session(engine) as session: + hero = Hero(name="Deadpond", secret_name="Dive Wilson") + print("The hero before saving in the DB") + print(hero) + print("The hero ID was already set") + print(hero.id) + session.add(hero) + session.commit() + session.refresh(hero) + print("After saving in the DB") + print(hero) + + +def select_hero(): + with Session(engine) as session: + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_2) + session.commit() + session.refresh(hero_2) + hero_id = hero_2.id + print("Created hero:") + print(hero_2) + print("Created hero ID:") + print(hero_id) + + statement = select(Hero).where(Hero.id == hero_id) + selected_hero = session.exec(statement).one() + print("Selected hero:") + print(selected_hero) + print("Selected hero ID:") + print(selected_hero.id) + + +def main() -> None: + create_db_and_tables() + create_hero() + select_hero() + + +if __name__ == "__main__": + main() diff --git a/docs_src/advanced/uuid/tutorial002.py b/docs_src/advanced/uuid/tutorial002.py new file mode 100644 index 0000000000..831725581b --- /dev/null +++ b/docs_src/advanced/uuid/tutorial002.py @@ -0,0 +1,64 @@ +import uuid +from typing import Union + +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Union[int, None] = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_hero(): + with Session(engine) as session: + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + print("The hero before saving in the DB") + print(hero_1) + print("The hero ID was already set") + print(hero_1.id) + session.add(hero_1) + session.commit() + session.refresh(hero_1) + print("After saving in the DB") + print(hero_1) + + +def select_hero(): + with Session(engine) as session: + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_2) + session.commit() + session.refresh(hero_2) + hero_id = hero_2.id + print("Created hero:") + print(hero_2) + print("Created hero ID:") + print(hero_id) + + selected_hero = session.get(Hero, hero_id) + print("Selected hero:") + print(selected_hero) + print("Selected hero ID:") + print(selected_hero.id) + + +def main() -> None: + create_db_and_tables() + create_hero() + select_hero() + + +if __name__ == "__main__": + main() diff --git a/docs_src/advanced/uuid/tutorial002_py310.py b/docs_src/advanced/uuid/tutorial002_py310.py new file mode 100644 index 0000000000..3ec8c80fa0 --- /dev/null +++ b/docs_src/advanced/uuid/tutorial002_py310.py @@ -0,0 +1,63 @@ +import uuid + +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_hero(): + with Session(engine) as session: + hero = Hero(name="Deadpond", secret_name="Dive Wilson") + print("The hero before saving in the DB") + print(hero) + print("The hero ID was already set") + print(hero.id) + session.add(hero) + session.commit() + session.refresh(hero) + print("After saving in the DB") + print(hero) + + +def select_hero(): + with Session(engine) as session: + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_2) + session.commit() + session.refresh(hero_2) + hero_id = hero_2.id + print("Created hero:") + print(hero_2) + print("Created hero ID:") + print(hero_id) + + selected_hero = session.get(Hero, hero_id) + print("Selected hero:") + print(selected_hero) + print("Selected hero ID:") + print(selected_hero.id) + + +def main() -> None: + create_db_and_tables() + create_hero() + select_hero() + + +if __name__ == "__main__": + main() diff --git a/mkdocs.yml b/mkdocs.yml index fa85062a8b..09c2b7f156 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -99,6 +99,7 @@ nav: - Advanced User Guide: - advanced/index.md - advanced/decimal.md + - advanced/uuid.md - alternatives.md - help.md - contributing.md diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 61ae35f7eb..5983974d77 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -140,5 +140,4 @@ 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 diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 505683f756..5755bbb4fd 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -51,7 +51,7 @@ 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 @@ 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") diff --git a/sqlmodel/sql/sqltypes.py b/sqlmodel/sql/sqltypes.py index 5a4bb04ef1..512daacbab 100644 --- a/sqlmodel/sql/sqltypes.py +++ b/sqlmodel/sql/sqltypes.py @@ -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 @@ def load_dialect_impl(self, dialect: Dialect) -> "types.TypeEngine[Any]": 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) diff --git a/tests/test_advanced/test_uuid/__init__.py b/tests/test_advanced/test_uuid/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_advanced/test_uuid/test_tutorial001.py b/tests/test_advanced/test_uuid/test_tutorial001.py new file mode 100644 index 0000000000..405195f8e9 --- /dev/null +++ b/tests/test_advanced/test_uuid/test_tutorial001.py @@ -0,0 +1,71 @@ +from unittest.mock import patch + +from dirty_equals import IsUUID +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function + + +def test_tutorial(clear_sqlmodel) -> None: + from docs_src.advanced.uuid import tutorial001 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + first_uuid = calls[1][0]["id"] + assert first_uuid == IsUUID(4) + + second_uuid = calls[7][0]["id"] + assert second_uuid == IsUUID(4) + + assert first_uuid != second_uuid + + assert calls == [ + ["The hero before saving in the DB"], + [ + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "id": first_uuid, + "age": None, + } + ], + ["The hero ID was already set"], + [first_uuid], + ["After saving in the DB"], + [ + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "age": None, + "id": first_uuid, + } + ], + ["Created hero:"], + [ + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + "id": second_uuid, + } + ], + ["Created hero ID:"], + [second_uuid], + ["Selected hero:"], + [ + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + "id": second_uuid, + } + ], + ["Selected hero ID:"], + [second_uuid], + ] diff --git a/tests/test_advanced/test_uuid/test_tutorial001_py310.py b/tests/test_advanced/test_uuid/test_tutorial001_py310.py new file mode 100644 index 0000000000..ee8cb085df --- /dev/null +++ b/tests/test_advanced/test_uuid/test_tutorial001_py310.py @@ -0,0 +1,72 @@ +from unittest.mock import patch + +from dirty_equals import IsUUID +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel) -> None: + from docs_src.advanced.uuid import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + first_uuid = calls[1][0]["id"] + assert first_uuid == IsUUID(4) + + second_uuid = calls[7][0]["id"] + assert second_uuid == IsUUID(4) + + assert first_uuid != second_uuid + + assert calls == [ + ["The hero before saving in the DB"], + [ + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "id": first_uuid, + "age": None, + } + ], + ["The hero ID was already set"], + [first_uuid], + ["After saving in the DB"], + [ + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "age": None, + "id": first_uuid, + } + ], + ["Created hero:"], + [ + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + "id": second_uuid, + } + ], + ["Created hero ID:"], + [second_uuid], + ["Selected hero:"], + [ + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + "id": second_uuid, + } + ], + ["Selected hero ID:"], + [second_uuid], + ] diff --git a/tests/test_advanced/test_uuid/test_tutorial002.py b/tests/test_advanced/test_uuid/test_tutorial002.py new file mode 100644 index 0000000000..cefd95ba49 --- /dev/null +++ b/tests/test_advanced/test_uuid/test_tutorial002.py @@ -0,0 +1,71 @@ +from unittest.mock import patch + +from dirty_equals import IsUUID +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function + + +def test_tutorial(clear_sqlmodel) -> None: + from docs_src.advanced.uuid import tutorial002 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + first_uuid = calls[1][0]["id"] + assert first_uuid == IsUUID(4) + + second_uuid = calls[7][0]["id"] + assert second_uuid == IsUUID(4) + + assert first_uuid != second_uuid + + assert calls == [ + ["The hero before saving in the DB"], + [ + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "id": first_uuid, + "age": None, + } + ], + ["The hero ID was already set"], + [first_uuid], + ["After saving in the DB"], + [ + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "age": None, + "id": first_uuid, + } + ], + ["Created hero:"], + [ + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + "id": second_uuid, + } + ], + ["Created hero ID:"], + [second_uuid], + ["Selected hero:"], + [ + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + "id": second_uuid, + } + ], + ["Selected hero ID:"], + [second_uuid], + ] diff --git a/tests/test_advanced/test_uuid/test_tutorial002_py310.py b/tests/test_advanced/test_uuid/test_tutorial002_py310.py new file mode 100644 index 0000000000..96f85c5333 --- /dev/null +++ b/tests/test_advanced/test_uuid/test_tutorial002_py310.py @@ -0,0 +1,72 @@ +from unittest.mock import patch + +from dirty_equals import IsUUID +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel) -> None: + from docs_src.advanced.uuid import tutorial002_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + first_uuid = calls[1][0]["id"] + assert first_uuid == IsUUID(4) + + second_uuid = calls[7][0]["id"] + assert second_uuid == IsUUID(4) + + assert first_uuid != second_uuid + + assert calls == [ + ["The hero before saving in the DB"], + [ + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "id": first_uuid, + "age": None, + } + ], + ["The hero ID was already set"], + [first_uuid], + ["After saving in the DB"], + [ + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "age": None, + "id": first_uuid, + } + ], + ["Created hero:"], + [ + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + "id": second_uuid, + } + ], + ["Created hero ID:"], + [second_uuid], + ["Selected hero:"], + [ + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + "id": second_uuid, + } + ], + ["Selected hero ID:"], + [second_uuid], + ]