✨ 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:
parent
3b889e09f7
commit
95936bb508
342
docs/advanced/uuid.md
Normal file
342
docs/advanced/uuid.md
Normal file
@ -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 <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>.
|
0
docs_src/advanced/uuid/__init__.py
Normal file
0
docs_src/advanced/uuid/__init__.py
Normal file
65
docs_src/advanced/uuid/tutorial001.py
Normal file
65
docs_src/advanced/uuid/tutorial001.py
Normal file
@ -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()
|
64
docs_src/advanced/uuid/tutorial001_py310.py
Normal file
64
docs_src/advanced/uuid/tutorial001_py310.py
Normal file
@ -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()
|
64
docs_src/advanced/uuid/tutorial002.py
Normal file
64
docs_src/advanced/uuid/tutorial002.py
Normal file
@ -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()
|
63
docs_src/advanced/uuid/tutorial002_py310.py
Normal file
63
docs_src/advanced/uuid/tutorial002_py310.py
Normal file
@ -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()
|
@ -99,6 +99,7 @@ nav:
|
|||||||
- Advanced User Guide:
|
- Advanced User Guide:
|
||||||
- advanced/index.md
|
- advanced/index.md
|
||||||
- advanced/decimal.md
|
- advanced/decimal.md
|
||||||
|
- advanced/uuid.md
|
||||||
- alternatives.md
|
- alternatives.md
|
||||||
- help.md
|
- help.md
|
||||||
- contributing.md
|
- contributing.md
|
||||||
|
@ -140,5 +140,4 @@ from .sql.expression import select as select
|
|||||||
from .sql.expression import tuple_ as tuple_
|
from .sql.expression import tuple_ as tuple_
|
||||||
from .sql.expression import type_coerce as type_coerce
|
from .sql.expression import type_coerce as type_coerce
|
||||||
from .sql.expression import within_group as within_group
|
from .sql.expression import within_group as within_group
|
||||||
from .sql.sqltypes import GUID as GUID
|
|
||||||
from .sql.sqltypes import AutoString as AutoString
|
from .sql.sqltypes import AutoString as AutoString
|
||||||
|
@ -51,7 +51,7 @@ from sqlalchemy.orm.attributes import set_attribute
|
|||||||
from sqlalchemy.orm.decl_api import DeclarativeMeta
|
from sqlalchemy.orm.decl_api import DeclarativeMeta
|
||||||
from sqlalchemy.orm.instrumentation import is_instrumented
|
from sqlalchemy.orm.instrumentation import is_instrumented
|
||||||
from sqlalchemy.sql.schema import MetaData
|
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 typing_extensions import Literal, deprecated, get_origin
|
||||||
|
|
||||||
from ._compat import ( # type: ignore[attr-defined]
|
from ._compat import ( # type: ignore[attr-defined]
|
||||||
@ -80,7 +80,7 @@ from ._compat import ( # type: ignore[attr-defined]
|
|||||||
sqlmodel_init,
|
sqlmodel_init,
|
||||||
sqlmodel_validate,
|
sqlmodel_validate,
|
||||||
)
|
)
|
||||||
from .sql.sqltypes import GUID, AutoString
|
from .sql.sqltypes import AutoString
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from pydantic._internal._model_construction import ModelMetaclass as ModelMetaclass
|
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),
|
scale=getattr(metadata, "decimal_places", None),
|
||||||
)
|
)
|
||||||
if issubclass(type_, uuid.UUID):
|
if issubclass(type_, uuid.UUID):
|
||||||
return GUID
|
return Uuid
|
||||||
raise ValueError(f"{type_} has no matching SQLAlchemy type")
|
raise ValueError(f"{type_} has no matching SQLAlchemy type")
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
import uuid
|
from typing import Any, cast
|
||||||
from typing import Any, Optional, cast
|
|
||||||
|
|
||||||
from sqlalchemy import CHAR, types
|
from sqlalchemy import types
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
|
||||||
from sqlalchemy.engine.interfaces import Dialect
|
from sqlalchemy.engine.interfaces import Dialect
|
||||||
from sqlalchemy.sql.type_api import TypeEngine
|
|
||||||
|
|
||||||
|
|
||||||
class AutoString(types.TypeDecorator): # type: ignore
|
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":
|
if impl.length is None and dialect.name == "mysql":
|
||||||
return dialect.type_descriptor(types.String(self.mysql_default_length))
|
return dialect.type_descriptor(types.String(self.mysql_default_length))
|
||||||
return super().load_dialect_impl(dialect)
|
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)
|
|
||||||
|
0
tests/test_advanced/test_uuid/__init__.py
Normal file
0
tests/test_advanced/test_uuid/__init__.py
Normal file
71
tests/test_advanced/test_uuid/test_tutorial001.py
Normal file
71
tests/test_advanced/test_uuid/test_tutorial001.py
Normal file
@ -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],
|
||||||
|
]
|
72
tests/test_advanced/test_uuid/test_tutorial001_py310.py
Normal file
72
tests/test_advanced/test_uuid/test_tutorial001_py310.py
Normal file
@ -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],
|
||||||
|
]
|
71
tests/test_advanced/test_uuid/test_tutorial002.py
Normal file
71
tests/test_advanced/test_uuid/test_tutorial002.py
Normal file
@ -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],
|
||||||
|
]
|
72
tests/test_advanced/test_uuid/test_tutorial002_py310.py
Normal file
72
tests/test_advanced/test_uuid/test_tutorial002_py310.py
Normal file
@ -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],
|
||||||
|
]
|
Loading…
x
Reference in New Issue
Block a user