--- /dev/null
+# 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 <abbr title="numbers in base 16 (instead of base 10), using letters from A to F to represent the numbers from 10 to 15">hexadecimal</abbr> 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 "<abbr title="Something that can be called as a function.">callable</abbr>"). 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 <a href="https://docs.pydantic.dev/latest/api/standard_library_types/#uuid" class="external-link" target="_blank">`UUID` types</a>.
+
+For the database, **SQLModel** internally uses <a href="https://docs.sqlalchemy.org/en/20/core/type_basics.html#sqlalchemy.types.Uuid" class="external-link" target="_blank">SQLAlchemy's `Uuid` type</a>.
+
+### 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**.
+
+<div class="termy">
+
+```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
+```
+
+</div>
+
+## Learn More
+
+You can learn more about **UUIDs** in:
+
+* The official <a href="https://docs.python.org/3/library/uuid.html" class="external-link" target="_blank">Python docs for UUID</a>.
+* The <a href="https://en.wikipedia.org/wiki/Universally_unique_identifier" class="external-link" target="_blank">Wikipedia for UUID</a>.
--- /dev/null
+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()
--- /dev/null
+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()
--- /dev/null
+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()
--- /dev/null
+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()
- Advanced User Guide:
- advanced/index.md
- advanced/decimal.md
+ - advanced/uuid.md
- alternatives.md
- help.md
- contributing.md
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
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]
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
scale=getattr(metadata, "decimal_places", None),
)
if issubclass(type_, uuid.UUID):
- return GUID
+ return Uuid
raise ValueError(f"{type_} has no matching SQLAlchemy type")
-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
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)
--- /dev/null
+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],
+ ]
--- /dev/null
+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],
+ ]
--- /dev/null
+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],
+ ]
--- /dev/null
+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],
+ ]