mirror of
https://github.com/PaiGramTeam/sqlmodel.git
synced 2024-11-25 09:27:40 +00:00
📝 Add docs annotations for source examples
This commit is contained in:
parent
e6308146f7
commit
dc331f649c
@ -0,0 +1,387 @@
|
|||||||
|
1. Create the `hero_1`.
|
||||||
|
|
||||||
|
**Doesn't generate any output**.
|
||||||
|
|
||||||
|
2. Create the `hero_2`.
|
||||||
|
|
||||||
|
**Doesn't generate any output**.
|
||||||
|
|
||||||
|
3. Create the `hero_3`.
|
||||||
|
|
||||||
|
**Doesn't generate any output**.
|
||||||
|
|
||||||
|
4. Print the line `"Before interacting with the database"`.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Before interacting with the database
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Print the `hero_1` before interacting with the database.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hero 1: id=None name='Deadpond' secret_name='Dive Wilson' age=None
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Print the `hero_2` before interacting with the database.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hero 2: id=None name='Spider-Boy' secret_name='Pedro Parqueador' age=None
|
||||||
|
```
|
||||||
|
|
||||||
|
7. Print the `hero_3` before interacting with the database.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hero 3: id=None name='Rusty-Man' secret_name='Tommy Sharp' age=48
|
||||||
|
```
|
||||||
|
|
||||||
|
8. Create the `Session` in a `with` block.
|
||||||
|
|
||||||
|
**Doesn't generate any output**.
|
||||||
|
|
||||||
|
9. Add the `hero_1` to the session.
|
||||||
|
|
||||||
|
This still doesn't save it to the database.
|
||||||
|
|
||||||
|
**Doesn't generate any output**.
|
||||||
|
|
||||||
|
10. Add the `hero_2` to the session.
|
||||||
|
|
||||||
|
This still doesn't save it to the database.
|
||||||
|
|
||||||
|
**Doesn't generate any output**.
|
||||||
|
|
||||||
|
11. Add the `hero_3` to the session.
|
||||||
|
|
||||||
|
This still doesn't save it to the database.
|
||||||
|
|
||||||
|
**Doesn't generate any output**.
|
||||||
|
|
||||||
|
12. Print the line `"After adding to the session"`.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
After adding to the session
|
||||||
|
```
|
||||||
|
|
||||||
|
13. Print the `hero_1` after adding it to the session.
|
||||||
|
|
||||||
|
It still has the same data as there hasn't been any interaction with the database yet. Notice that the `id` is still `None`.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hero 1: id=None name='Deadpond' secret_name='Dive Wilson' age=None
|
||||||
|
```
|
||||||
|
|
||||||
|
14. Print the `hero_2` after adding it to the session.
|
||||||
|
|
||||||
|
It still has the same data as there hasn't been any interaction with the database yet. Notice that the `id` is still `None`.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hero 2: id=None name='Spider-Boy' secret_name='Pedro Parqueador' age=None
|
||||||
|
```
|
||||||
|
|
||||||
|
15. Print the `hero_3` after adding it to the session.
|
||||||
|
|
||||||
|
It still has the same data as there hasn't been any interaction with the database yet. Notice that the `id` is still `None`.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hero 3: id=None name='Rusty-Man' secret_name='Tommy Sharp' age=48
|
||||||
|
```
|
||||||
|
|
||||||
|
16. `commit` the **session**.
|
||||||
|
|
||||||
|
This will **save** all the data to the database. The **session** will use the **engine** to run a lot of SQL.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO Engine BEGIN (implicit)
|
||||||
|
INFO Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?)
|
||||||
|
INFO Engine [generated in 0.00018s] ('Deadpond', 'Dive Wilson', None)
|
||||||
|
INFO Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?)
|
||||||
|
INFO Engine [cached since 0.0008968s ago] ('Spider-Boy', 'Pedro Parqueador', None)
|
||||||
|
INFO Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?)
|
||||||
|
INFO Engine [cached since 0.001143s ago] ('Rusty-Man', 'Tommy Sharp', 48)
|
||||||
|
INFO Engine COMMIT
|
||||||
|
```
|
||||||
|
|
||||||
|
17. Print the line `"After committing the session"`.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
After committing the session
|
||||||
|
```
|
||||||
|
|
||||||
|
18. Print the `hero_1` after committing the session.
|
||||||
|
|
||||||
|
The `hero_1` is now internally marked as expired, and until it is refreshed, it looks like if it didn't contain any data.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hero 1:
|
||||||
|
```
|
||||||
|
|
||||||
|
19. Print the `hero_2` after committing the session.
|
||||||
|
|
||||||
|
The `hero_2` is now internally marked as expired, and until it is refreshed, it looks like if it didn't contain any data.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hero 2:
|
||||||
|
```
|
||||||
|
|
||||||
|
20. Print the `hero_3` after committing the session.
|
||||||
|
|
||||||
|
The `hero_3` is now internally marked as expired, and until it is refreshed, it looks like if it didn't contain any data.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hero 3:
|
||||||
|
```
|
||||||
|
|
||||||
|
21. Print the line `"After commiting the session, show IDs"`.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
After committing the session, show IDs
|
||||||
|
```
|
||||||
|
|
||||||
|
22. Print the `hero_1.id`. A lot happens here.
|
||||||
|
|
||||||
|
Because we are accessing the attribute `id` of `hero_1`, **SQLModel** (actually SQLAlchemy) can detect that we are trying to access data from the `hero_1`.
|
||||||
|
|
||||||
|
It then detects that `hero_1` is currently associated with a **session** (because we added it to the session and committed it), and it is marked as expired.
|
||||||
|
|
||||||
|
Then with the **session**, it uses the **engine** to execute all the SQL to fetch the data for this object from the database.
|
||||||
|
|
||||||
|
Next it updates the object with the new data and marks it internally as "fresh" or "not expired".
|
||||||
|
|
||||||
|
Finally, it makes the ID value available for the rest of the Python expression. In this case, the Python expression just prints the ID.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO Engine BEGIN (implicit)
|
||||||
|
INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age
|
||||||
|
FROM hero
|
||||||
|
WHERE hero.id = ?
|
||||||
|
INFO Engine [generated in 0.00017s] (1,)
|
||||||
|
|
||||||
|
Hero 1 ID: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
23. Print the `hero_2.id`.
|
||||||
|
|
||||||
|
A lot happens here, all the same stuff that happened at point 22, but for this `hero_2` object.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age
|
||||||
|
FROM hero
|
||||||
|
WHERE hero.id = ?
|
||||||
|
INFO Engine [cached since 0.001245s ago] (2,)
|
||||||
|
|
||||||
|
Hero 2 ID: 2
|
||||||
|
```
|
||||||
|
|
||||||
|
24. Print the `hero_3.id`.
|
||||||
|
|
||||||
|
A lot happens here, all the same stuff that happened at point 22, but for this `hero_3` object.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age
|
||||||
|
FROM hero
|
||||||
|
WHERE hero.id = ?
|
||||||
|
INFO Engine [cached since 0.002215s ago] (3,)
|
||||||
|
|
||||||
|
|
||||||
|
Hero 3 ID: 3
|
||||||
|
```
|
||||||
|
|
||||||
|
25. Print the line `"After committing the session, show names"`.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
After committing the session, show names
|
||||||
|
```
|
||||||
|
|
||||||
|
26. Print the `hero_1.name`.
|
||||||
|
|
||||||
|
Because `hero_1` is still fresh, no additional data is fetched, no additional SQL is executed, and the name is available.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hero 1 name: Deadpond
|
||||||
|
```
|
||||||
|
|
||||||
|
27. Print the `hero_2.name`.
|
||||||
|
|
||||||
|
Because `hero_2` is still fresh, no additional data is fetched, no additional SQL is executed, and the name is available.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hero 2 name: Spider-Boy
|
||||||
|
```
|
||||||
|
|
||||||
|
28. Print the `hero_3.name`.
|
||||||
|
|
||||||
|
Because `hero_3` is still fresh, no additional data is fetched, no additional SQL is executed, and the name is available.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hero 3 name: Rusty-Man
|
||||||
|
```
|
||||||
|
|
||||||
|
29. Explicitly refresh the `hero_1` object.
|
||||||
|
|
||||||
|
The **session** will use the **engine** to execute the SQL necessary to fetch fresh data from the database for the `hero_1` object.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
|
||||||
|
FROM hero
|
||||||
|
WHERE hero.id = ?
|
||||||
|
INFO Engine [generated in 0.00024s] (1,)
|
||||||
|
```
|
||||||
|
|
||||||
|
30. Explicitly refresh the `hero_2` object.
|
||||||
|
|
||||||
|
The **session** will use the **engine** to execute the SQL necessary to fetch fresh data from the database for the `hero_2` object.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
|
||||||
|
FROM hero
|
||||||
|
WHERE hero.id = ?
|
||||||
|
INFO Engine [cached since 0.001487s ago] (2,)
|
||||||
|
```
|
||||||
|
|
||||||
|
31. Explicitly refresh the `hero_3` object.
|
||||||
|
|
||||||
|
The **session** will use the **engine** to execute the SQL necessary to fetch fresh data from the database for the `hero_3` object.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
|
||||||
|
FROM hero
|
||||||
|
WHERE hero.id = ?
|
||||||
|
INFO Engine [cached since 0.002377s ago] (3,)
|
||||||
|
```
|
||||||
|
|
||||||
|
32. Print the line `"After refreshing the heroes"`.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
After refreshing the heroes
|
||||||
|
```
|
||||||
|
|
||||||
|
33. Print the `hero_1`.
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
Even if the `hero_1` wasn't fresh, this would **not** trigger a `refresh` making the **session** use the **engine** to fetch data from the database because it is not accessing an attribute.
|
||||||
|
|
||||||
|
Because the `hero_1` is fresh it has all it's data available.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hero 1: age=None id=1 name='Deadpond' secret_name='Dive Wilson'
|
||||||
|
```
|
||||||
|
|
||||||
|
34. Print the `hero_2`.
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
Even if the `hero_2` wasn't fresh, this would **not** trigger a `refresh` making the **session** use the **engine** to fetch data from the database because it is not accessing an attribute.
|
||||||
|
|
||||||
|
Because the `hero_2` is fresh it has all it's data available.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hero 2: age=None id=2 name='Spider-Boy' secret_name='Pedro Parqueador'
|
||||||
|
```
|
||||||
|
|
||||||
|
35. Print the `hero_3`.
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
Even if the `hero_3` wasn't fresh, this would **not** trigger a `refresh` making the **session** use the **engine** to fetch data from the database because it is not accessing an attribute.
|
||||||
|
|
||||||
|
Because the `hero_3` is fresh it has all it's data available.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hero 3: age=48 id=3 name='Rusty-Man' secret_name='Tommy Sharp'
|
||||||
|
```
|
||||||
|
|
||||||
|
36. The `with` block ends here (there's no more indented code), so the **session** is closed, running all it's closing code.
|
||||||
|
|
||||||
|
This includes doing a `ROLLBACK` of any possible transaction that could have been started.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO Engine ROLLBACK
|
||||||
|
```
|
||||||
|
|
||||||
|
37. Print the line `"After the session closes"`.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
After the session closes
|
||||||
|
```
|
||||||
|
|
||||||
|
38. Print the `hero_1` after closing the session.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hero 1: age=None id=1 name='Deadpond' secret_name='Dive Wilson'
|
||||||
|
```
|
||||||
|
|
||||||
|
39. Print the `hero_2` after closing the session.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hero 2: age=None id=2 name='Spider-Boy' secret_name='Pedro Parqueador'
|
||||||
|
```
|
||||||
|
|
||||||
|
40. Print the `hero_3` after closing the session.
|
||||||
|
|
||||||
|
Generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hero 3: age=48 id=3 name='Rusty-Man' secret_name='Tommy Sharp'
|
||||||
|
```
|
@ -0,0 +1,75 @@
|
|||||||
|
1. Import `Optional` from `typing` to declare fields that could be `None`.
|
||||||
|
2. Import the things we will need from `sqlmodel`: `Field`, `SQLModel`, `create_engine`.
|
||||||
|
3. Create the `Hero` model class, representing the `hero` table in the database.
|
||||||
|
|
||||||
|
And also mark this class as a **table model** with `table=True`.
|
||||||
|
|
||||||
|
4. Create the `id` field:
|
||||||
|
|
||||||
|
It could be `None` until the database assigns a value to it, so we annotate it with `Optional`.
|
||||||
|
|
||||||
|
It is a **primary key**, so we use `Field()` and the argument `primary_key=True`.
|
||||||
|
|
||||||
|
5. Create the `name` field.
|
||||||
|
|
||||||
|
It is required, so there's no default value, and it's not `Optional`.
|
||||||
|
|
||||||
|
6. Create the `secret_name` field.
|
||||||
|
|
||||||
|
Also required.
|
||||||
|
|
||||||
|
7. Create the `age` field.
|
||||||
|
|
||||||
|
It is not required, the default value is `None`.
|
||||||
|
|
||||||
|
In the database, the default value will be `NULL`, the SQL equivalent of `None`.
|
||||||
|
|
||||||
|
As this field could be `None` (and `NULL` in the database), we annotate it with `Optional`.
|
||||||
|
|
||||||
|
8. Write the name of the database file.
|
||||||
|
9. Use the name of the database file to create the database URL.
|
||||||
|
10. Create the engine using the URL.
|
||||||
|
|
||||||
|
This doesn't create the database yet, no file or table is created at this point, only the **engine** object that will handle the connections with this specific database, and with specific support for SQLite (based on the URL).
|
||||||
|
|
||||||
|
11. Put the code that creates side effects in a function.
|
||||||
|
|
||||||
|
In this case, only one line that creates the database file with the table.
|
||||||
|
|
||||||
|
12. Create all the tables that were automatically registered in `SQLModel.metadata`.
|
||||||
|
|
||||||
|
13. Add a main block, or "Top-level script environment".
|
||||||
|
|
||||||
|
And put some logic to be executed when this is called directly with Python, as in:
|
||||||
|
|
||||||
|
<div class="termy">
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ python app.py
|
||||||
|
|
||||||
|
// Execute all the stuff and show the output
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
...but that is not executed when importing something from this module, like:
|
||||||
|
|
||||||
|
```Python
|
||||||
|
from app import Hero
|
||||||
|
```
|
||||||
|
|
||||||
|
14. In this main block, call the function that creates the database file and the table.
|
||||||
|
|
||||||
|
This way when we call it with:
|
||||||
|
|
||||||
|
<div class="termy">
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ python app.py
|
||||||
|
|
||||||
|
// Doing stuff ✨
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
...it will create the database file and the table.
|
96
docs_src/tutorial/delete/annotations/en/tutorial002.md
Normal file
96
docs_src/tutorial/delete/annotations/en/tutorial002.md
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
1. Select the hero we will delete.
|
||||||
|
|
||||||
|
2. Execute the query with the select statement object.
|
||||||
|
|
||||||
|
This generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO Engine BEGIN (implicit)
|
||||||
|
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
|
||||||
|
FROM hero
|
||||||
|
WHERE hero.name = ?
|
||||||
|
INFO Engine [no key 0.00011s] ('Spider-Youngster',)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Get one hero object, expecting exactly one.
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
This ensures there's no more than one, and that there's exactly one, not `None`.
|
||||||
|
|
||||||
|
This would never return `None`, instead it would raise an exception.
|
||||||
|
|
||||||
|
4. Print the hero object.
|
||||||
|
|
||||||
|
This generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hero: name='Spider-Youngster' secret_name='Pedro Parqueador' age=16 id=2
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Delete the hero from the **session**.
|
||||||
|
|
||||||
|
This marks the hero as deleted from the session, but it will not be removed from the database until we **commit** the changes.
|
||||||
|
|
||||||
|
6. Commit the session.
|
||||||
|
|
||||||
|
This saves the changes in the session, including deleting this row.
|
||||||
|
|
||||||
|
It generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO Engine DELETE FROM hero WHERE hero.id = ?
|
||||||
|
INFO Engine [generated in 0.00020s] (2,)
|
||||||
|
INFO Engine COMMIT
|
||||||
|
```
|
||||||
|
|
||||||
|
7. Print the deleted hero object.
|
||||||
|
|
||||||
|
The hero is deleted in the database. And is marked as deleted in the session.
|
||||||
|
|
||||||
|
But we still have the object in memory with its data, so we can use it to print it.
|
||||||
|
|
||||||
|
This generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Deleted hero: name='Spider-Youngster' secret_name='Pedro Parqueador' age=16 id=2
|
||||||
|
```
|
||||||
|
|
||||||
|
8. Select the same hero again.
|
||||||
|
|
||||||
|
We'll do this to confirm if the hero is really deleted.
|
||||||
|
|
||||||
|
9. Execute the select statement.
|
||||||
|
|
||||||
|
This generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO Engine BEGIN (implicit)
|
||||||
|
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
|
||||||
|
FROM hero
|
||||||
|
WHERE hero.name = ?
|
||||||
|
INFO Engine [no key 0.00013s] ('Spider-Youngster',)
|
||||||
|
```
|
||||||
|
|
||||||
|
10. Get the "first" item from the `results`.
|
||||||
|
|
||||||
|
If no items were found, this will return `None`, which is what we expect.
|
||||||
|
|
||||||
|
11. Check if the first item from the results is `None`.
|
||||||
|
|
||||||
|
12. If this first item is indeed `None`, it means that it was correctly deleted from the database.
|
||||||
|
|
||||||
|
Now we can print a message to confirm.
|
||||||
|
|
||||||
|
This generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
There's no hero named Spider-Youngster
|
||||||
|
```
|
||||||
|
|
||||||
|
13. This is the end of the `with` block, here the **session** executes its closing code.
|
||||||
|
|
||||||
|
This generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO Engine ROLLBACK
|
||||||
|
```
|
@ -0,0 +1,17 @@
|
|||||||
|
1. Import the `app` from the the `main` module.
|
||||||
|
|
||||||
|
2. We create a `TestClient` for the FastAPI `app` and put it in the variable `client`.
|
||||||
|
|
||||||
|
3. Then we use use this `client` to **talk to the API** and send a `POST` HTTP operation, creating a new hero.
|
||||||
|
|
||||||
|
4. Then we get the **JSON data** from the response and put it in the variable `data`.
|
||||||
|
|
||||||
|
5. Next we start testing the results with `assert` statements, we check that the status code of the response is `200`.
|
||||||
|
|
||||||
|
6. We check that the `name` of the hero created is `"Deadpond"`.
|
||||||
|
|
||||||
|
7. We check that the `secret_name` of the hero created is `"Dive Wilson"`.
|
||||||
|
|
||||||
|
8. We check that the `age` of the hero created is `None`, because we didn't send an age.
|
||||||
|
|
||||||
|
9. We check that the hero created has an `id` created by the database, so it's not `None`.
|
@ -0,0 +1,25 @@
|
|||||||
|
1. Import the `get_session` dependency from the the `main` module.
|
||||||
|
|
||||||
|
2. Define the new function that will be the new **dependency override**.
|
||||||
|
|
||||||
|
3. This function will return a different **session** than the one that would be returned by the original `get_session` function.
|
||||||
|
|
||||||
|
We haven't seen how this new **session** object is created yet, but the point is that this is a different session than the original one from the app.
|
||||||
|
|
||||||
|
This session is attached to a different **engine**, and that different **engine** uses a different URL, for a database just for testing.
|
||||||
|
|
||||||
|
We haven't defined that new **URL** nor the new **engine** yet, but here we already see the that this object `session` will override the one returned by the original dependency `get_session()`.
|
||||||
|
|
||||||
|
4. Then, the FastAPI `app` object has an attribute `app.dependency_overrides`.
|
||||||
|
|
||||||
|
This attribute is a dictionary, and we can put dependency overrides in it by passing, as the **key**, the **original dependency function**, and as the **value**, the **new overriding dependency function**.
|
||||||
|
|
||||||
|
So, here we are telling the FastAPI app to use `get_session_override` instead of `get_session` in all the places in the code that depend on `get_session`, that is, all the parameters with something like:
|
||||||
|
|
||||||
|
```Python
|
||||||
|
session: Session = Depends(get_session)
|
||||||
|
```
|
||||||
|
|
||||||
|
5. After we are done with the dependency override, we can restore the application back to normal, by removing all the values in this dictionary `app.dependency_overrides`.
|
||||||
|
|
||||||
|
This way whenever a *path operation function* needs the dependency FastAPI will use the original one instead of the override.
|
@ -0,0 +1,37 @@
|
|||||||
|
1. Here's a subtle thing to notice.
|
||||||
|
|
||||||
|
Remember that [Order Matters](../create-db-and-table.md#sqlmodel-metadata-order-matters){.internal-link target=_blank} and we need to make sure all the **SQLModel** models are already defined and **imported** before calling `.create_all()`.
|
||||||
|
|
||||||
|
IN this line, by importing something, *anything*, from `.main`, the code in `.main` will be executed, including the definition of the **table models**, and that will automatically register them in `SQLModel.metadata`.
|
||||||
|
|
||||||
|
2. Here we create a new **engine**, completely different from the one in `main.py`.
|
||||||
|
|
||||||
|
This is the engine we will use for the tests.
|
||||||
|
|
||||||
|
We use the new URL of the database for tests:
|
||||||
|
|
||||||
|
```
|
||||||
|
sqlite:///testing.db
|
||||||
|
```
|
||||||
|
|
||||||
|
And again, we use the connection argument `check_same_thread=False`.
|
||||||
|
|
||||||
|
3. Then we call:
|
||||||
|
|
||||||
|
```Python
|
||||||
|
SQLModel.metadata.create_all(engine)
|
||||||
|
```
|
||||||
|
|
||||||
|
...to make sure we create all the tables in the new testing database.
|
||||||
|
|
||||||
|
The **table models** are registered in `SQLModel.metadata` just because we imported *something* from `.main`, and the code in `.main` was executed, creating the classes for the **table models** and automatically registering them in `SQLModel.metadata`.
|
||||||
|
|
||||||
|
So, by the point we call this method, the **table models** are already registered there. 💯
|
||||||
|
|
||||||
|
4. Here's where we create the custom **session** object for this test in a `with` block.
|
||||||
|
|
||||||
|
It uses the new custom **engine** we created, so anything that uses this session will be using the testing database.
|
||||||
|
|
||||||
|
5. Now, back to the dependency override, it is just returning the same **session** object from outside, that's it, that's the whole trick.
|
||||||
|
|
||||||
|
6. By this point, the testing **session** `with` block finishes, and the session is closed, the file is closed, etc.
|
@ -0,0 +1,26 @@
|
|||||||
|
1. Import `StaticPool` from `sqlmodel`, we will use it in a bit.
|
||||||
|
|
||||||
|
2. For the **SQLite URL**, don't write any file name, leave it empty.
|
||||||
|
|
||||||
|
So, instead of:
|
||||||
|
|
||||||
|
```
|
||||||
|
sqlite:///testing.db
|
||||||
|
```
|
||||||
|
|
||||||
|
...just write:
|
||||||
|
|
||||||
|
```
|
||||||
|
sqlite://
|
||||||
|
```
|
||||||
|
|
||||||
|
This is enough to tell **SQLModel** (actually SQLAlchemy) that we want to use an **in-memory SQLite database**.
|
||||||
|
|
||||||
|
3. Remember that we told the **low-level** library in charge of communicating with SQLite that we want to be able to **access the database from different threads** with `check_same_thread=False`?
|
||||||
|
|
||||||
|
Now that we use an **in-memory database**, we need to also tell SQLAlchemy that we want to be able to use the **same in-memory database** object from different threads.
|
||||||
|
|
||||||
|
We tell it that with the `poolclass=StaticPool` parameter.
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
You can read more details in the <a href="https://docs.sqlalchemy.org/en/14/dialects/sqlite.html#using-a-memory-database-in-multiple-threads" class="external-link" target="_blank">SQLAlchemy documentation about Using a Memory Database in Multiple Threads</a>
|
@ -0,0 +1,41 @@
|
|||||||
|
1. Import `pytest`.
|
||||||
|
|
||||||
|
2. Use the `@pytest.fixture()` decorator on top of the function to tell pytest that this is a **fixture** function (equivalent to a FastAPI dependency).
|
||||||
|
|
||||||
|
We also give it a name of `"session"`, this will be important in the testing function.
|
||||||
|
|
||||||
|
3. Create the fixture function. This is equivalent to a FastAPI dependency function.
|
||||||
|
|
||||||
|
In this fixture we create the custom **engine**, with the in-memory database, we create the tables, and we create the **session**.
|
||||||
|
|
||||||
|
Then we `yield` the `session` object.
|
||||||
|
|
||||||
|
4. The thing that we `return` or `yield` is what will be available to the test function, in this case, the `session` object.
|
||||||
|
|
||||||
|
Here we use `yield` so that **pytest** comes back to execute "the rest of the code" in this function once the testing function is done.
|
||||||
|
|
||||||
|
We don't have any more visible "rest of the code" after the `yield`, but we have the end of the `with` block that will close the **session**.
|
||||||
|
|
||||||
|
By using `yield`, pytest will:
|
||||||
|
|
||||||
|
* run the first part
|
||||||
|
* create the **session** object
|
||||||
|
* give it to the test function
|
||||||
|
* run the test function
|
||||||
|
* once the test function is done, it will continue here, right after the `yield`, and will correctly close the **session** object in the end of the `with` block.
|
||||||
|
|
||||||
|
5. Now, in the test function, to tell **pytest** that this test wants to get the fixture, instead of declaring something like in FastAPI with:
|
||||||
|
|
||||||
|
```Python
|
||||||
|
session: Session = Depends(session_fixture)
|
||||||
|
```
|
||||||
|
|
||||||
|
...the way we tell pytest what is the fixture that we want is by using the **exact same name** of the fixture.
|
||||||
|
|
||||||
|
In this case, we named it `session`, so the parameter has to be exactly named `session` for it to work.
|
||||||
|
|
||||||
|
We also add the type annotation `session: Session` so that we can get autocompletion and inline error checks in our editor.
|
||||||
|
|
||||||
|
6. Now in the dependency override function, we just return the same `session` object that came from outside it.
|
||||||
|
|
||||||
|
The `session` object comes from the parameter passed to the test function, and we just re-use it and return it here in the dependency override.
|
@ -0,0 +1,23 @@
|
|||||||
|
1. Create the new fixture named `"client"`.
|
||||||
|
|
||||||
|
2. This **client fixture**, in turn, also requires the **session fixture**.
|
||||||
|
|
||||||
|
3. Now we create the **dependency override** inside the client fixture.
|
||||||
|
|
||||||
|
4. Set the **dependency override** in the `app.dependency_overrides` dictionary.
|
||||||
|
|
||||||
|
5. Create the `TestClient` with the **FastAPI** `app`.
|
||||||
|
|
||||||
|
6. `yield` the `TestClient` instance.
|
||||||
|
|
||||||
|
By using `yield`, after the test function is done, pytest will come back to execute the rest of the code after `yield`.
|
||||||
|
|
||||||
|
7. This is the cleanup code, after `yield`, and after the test function is done.
|
||||||
|
|
||||||
|
Here we clear the dependency overrides (here it's only one) in the FastAPI `app`.
|
||||||
|
|
||||||
|
8. Now the test function requires the **client fixture**.
|
||||||
|
|
||||||
|
And inside the test function, the code is quite **simple**, we just use the `TestClient` to make requests to the API, check the data, and that's it.
|
||||||
|
|
||||||
|
The fixtures take care of all the **setup** and **cleanup** code.
|
57
docs_src/tutorial/insert/annotations/en/tutorial003.md
Normal file
57
docs_src/tutorial/insert/annotations/en/tutorial003.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
1. We use a function `create_heroes()` to put this logic together.
|
||||||
|
|
||||||
|
2. Create each of the objects/instances of the `Hero` model.
|
||||||
|
|
||||||
|
Each of them represents the data for one row.
|
||||||
|
|
||||||
|
3. Use a `with` block to create a `Session` using the `engine`.
|
||||||
|
|
||||||
|
The new **sesion** will be assigned to the variable `session`.
|
||||||
|
|
||||||
|
And it will be automatically closed when the `with` block is finished.
|
||||||
|
|
||||||
|
4. Add each of the objects/instances to the **session**.
|
||||||
|
|
||||||
|
Each of these objects represents a row in the database.
|
||||||
|
|
||||||
|
They are all waiting there in the session to be saved.
|
||||||
|
|
||||||
|
5. **Commit** the changes to the database.
|
||||||
|
|
||||||
|
This will actually send the data to the database.
|
||||||
|
|
||||||
|
It will start a transaction automatically and save all the data in a single batch.
|
||||||
|
|
||||||
|
6. By this point, after the `with` block is finished, the **session** is automatically closed.
|
||||||
|
|
||||||
|
7. We have a `main()` function with all the code that should be executed when the program is called as a **script from the console**.
|
||||||
|
|
||||||
|
That way we can add more code later to this function.
|
||||||
|
|
||||||
|
We then put this function `main()` in the main block below.
|
||||||
|
|
||||||
|
And as it is a single function, other Python files could **import it** and call it directly.
|
||||||
|
|
||||||
|
8. In this `main()` function, we are also creating the database and the tables.
|
||||||
|
|
||||||
|
In the previous version, this function was called directly in the main block.
|
||||||
|
|
||||||
|
But now it is just called in the `main()` function.
|
||||||
|
|
||||||
|
9. And now we are also creating the heroes in this `main()` function.
|
||||||
|
|
||||||
|
10. We still have a main block to execute some code when the program is run as a script from the command line, like:
|
||||||
|
|
||||||
|
<div class="termy">
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ python app.py
|
||||||
|
|
||||||
|
// Do whatever is in the main block 🚀
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
11. There's a single `main()` function now that contains all the code that should be executed when running the program from the console.
|
||||||
|
|
||||||
|
So this is all we need to have in the main block. Just call the `main()` function.
|
63
docs_src/tutorial/select/annotations/en/tutorial002.md
Normal file
63
docs_src/tutorial/select/annotations/en/tutorial002.md
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
1. Import from `sqlmodel` everything we will use, including the new `select()` function.
|
||||||
|
|
||||||
|
2. Create the `Hero` class model, representing the `hero` table.
|
||||||
|
|
||||||
|
3. Create the **engine**, we should use a single one shared by all the application code, and that's what we are doing here.
|
||||||
|
|
||||||
|
4. Create all the tables for the models registered in `SQLModel.metadata`.
|
||||||
|
|
||||||
|
This also creates the database if it doesn't exist already.
|
||||||
|
|
||||||
|
5. Create each one of the `Hero` objects.
|
||||||
|
|
||||||
|
You might not have this in your version if you had already created the data in the database.
|
||||||
|
|
||||||
|
6. Create a new **session** and use it to `add` the heroes to the database, and then `commit` the changes.
|
||||||
|
|
||||||
|
7. Create a new **session** to query data.
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
Notice that this is a new **session** independent from the one in the other function above.
|
||||||
|
|
||||||
|
But it still uses the same **engine**. We still have one engine for the whole application.
|
||||||
|
|
||||||
|
8. Use the `select()` function to create a statement selecting all the `Hero` objects.
|
||||||
|
|
||||||
|
This selects all the rows in the `hero` table.
|
||||||
|
|
||||||
|
9. Use `session.exec(statement)` to make the **session** use the **engine** to execute the internal SQL statement.
|
||||||
|
|
||||||
|
This will go to the database, execute that SQL, and get the results back.
|
||||||
|
|
||||||
|
It returns a special iterable object that we put in the variable `results`.
|
||||||
|
|
||||||
|
This generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO Engine BEGIN (implicit)
|
||||||
|
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
|
||||||
|
FROM hero
|
||||||
|
INFO Engine [no key 0.00032s] ()
|
||||||
|
```
|
||||||
|
|
||||||
|
10. Iterate for each `Hero` object in the `results`.
|
||||||
|
|
||||||
|
11. Print each `hero`.
|
||||||
|
|
||||||
|
The 3 iterations in the `for` loop will generate this output:
|
||||||
|
|
||||||
|
```
|
||||||
|
id=1 name='Deadpond' age=None secret_name='Dive Wilson'
|
||||||
|
id=2 name='Spider-Boy' age=None secret_name='Pedro Parqueador'
|
||||||
|
id=3 name='Rusty-Man' age=48 secret_name='Tommy Sharp'
|
||||||
|
```
|
||||||
|
|
||||||
|
12. At this point, after the `with` block, the **session** is closed.
|
||||||
|
|
||||||
|
This generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO Engine ROLLBACK
|
||||||
|
```
|
||||||
|
|
||||||
|
13. Add this function `select_heroes()` to the `main()` function so that it is called when we run this program from the command line.
|
68
docs_src/tutorial/update/annotations/en/tutorial002.md
Normal file
68
docs_src/tutorial/update/annotations/en/tutorial002.md
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
1. Select the hero we will work with.
|
||||||
|
|
||||||
|
2. Execute the query with the select statement object.
|
||||||
|
|
||||||
|
This generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
|
||||||
|
FROM hero
|
||||||
|
WHERE hero.name = ?
|
||||||
|
INFO Engine [no key 0.00017s] ('Spider-Boy',)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Get one hero object, expecting exactly one.
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
This ensures there's no more than one, and that there's exactly one, not `None`.
|
||||||
|
|
||||||
|
This would never return `None`, instead it would raise an exception.
|
||||||
|
|
||||||
|
4. Print the hero object.
|
||||||
|
|
||||||
|
This generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hero: name='Spider-Boy' secret_name='Pedro Parqueador' age=None id=2
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Set the hero's age field to the new value `16`.
|
||||||
|
|
||||||
|
Now the `hero` object in memory has a different value for the age, but it is still not saved to the database.
|
||||||
|
|
||||||
|
6. Add the hero to the session.
|
||||||
|
|
||||||
|
This puts it in that temporary place in the session before committing.
|
||||||
|
|
||||||
|
But it's still not saved in the database yet.
|
||||||
|
|
||||||
|
7. Commit the session.
|
||||||
|
|
||||||
|
This saves the updated hero to the database.
|
||||||
|
|
||||||
|
And this generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO Engine UPDATE hero SET age=? WHERE hero.id = ?
|
||||||
|
INFO Engine [generated in 0.00017s] (16, 2)
|
||||||
|
INFO Engine COMMIT
|
||||||
|
```
|
||||||
|
|
||||||
|
8. Refresh the hero object to have the recent data, including the age we just committed.
|
||||||
|
|
||||||
|
This generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
|
||||||
|
FROM hero
|
||||||
|
WHERE hero.id = ?
|
||||||
|
INFO Engine [generated in 0.00018s] (2,)
|
||||||
|
```
|
||||||
|
|
||||||
|
9. Print the updated hero object.
|
||||||
|
|
||||||
|
This generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Updated hero: name='Spider-Boy' secret_name='Pedro Parqueador' age=16 id=2
|
||||||
|
```
|
159
docs_src/tutorial/update/annotations/en/tutorial004.md
Normal file
159
docs_src/tutorial/update/annotations/en/tutorial004.md
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
1. Select the hero `Spider-Boy`.
|
||||||
|
|
||||||
|
2. Execute the select statement.
|
||||||
|
|
||||||
|
This generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
|
||||||
|
FROM hero
|
||||||
|
WHERE hero.name = ?
|
||||||
|
INFO Engine [no key 0.00018s] ('Spider-Boy',)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Get one hero object, the only one that should be there for **Spider-Boy**.
|
||||||
|
|
||||||
|
4. Print this hero.
|
||||||
|
|
||||||
|
This generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hero 1: name='Spider-Boy' secret_name='Pedro Parqueador' age=None id=2
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Select another hero.
|
||||||
|
|
||||||
|
6. Execute the select statement.
|
||||||
|
|
||||||
|
This generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO Engine BEGIN (implicit)
|
||||||
|
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
|
||||||
|
FROM hero
|
||||||
|
WHERE hero.name = ?
|
||||||
|
INFO Engine [no key 0.00020s] ('Captain North America',)
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
See the `BEGIN` at the top?
|
||||||
|
|
||||||
|
This is SQLAlchemy automatically starting a transaction for us.
|
||||||
|
|
||||||
|
This way, we could revert the last changes (if there were some) if we wanted to, even if the SQL to create them was already sent to the database.
|
||||||
|
|
||||||
|
7. Get one hero object for this new query.
|
||||||
|
|
||||||
|
The only one that should be there for **Captain North America**.
|
||||||
|
|
||||||
|
8. Print this second hero.
|
||||||
|
|
||||||
|
This generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hero 2: name='Captain North America' secret_name='Esteban Rogelios' age=93 id=7
|
||||||
|
```
|
||||||
|
|
||||||
|
9. Update the age for the first hero.
|
||||||
|
|
||||||
|
Set the value of the attribute `age` to `16`.
|
||||||
|
|
||||||
|
This updates the hero object in memory, but not yet in the database.
|
||||||
|
|
||||||
|
10. Update the name of the first hero.
|
||||||
|
|
||||||
|
Now the name of the hero will not be `"Spider-Boy"` but `"Spider-Youngster"`.
|
||||||
|
|
||||||
|
Also, this updates the object in memory, but not yet in the database.
|
||||||
|
|
||||||
|
11. Add this first hero to the session.
|
||||||
|
|
||||||
|
This puts it in the temporary space in the **session** before committing it to the database.
|
||||||
|
|
||||||
|
It is not saved yet.
|
||||||
|
|
||||||
|
12. Update the name of the second hero.
|
||||||
|
|
||||||
|
Now the hero has a bit more precision in the name. 😜
|
||||||
|
|
||||||
|
This updates the object in memory, but not yet in the database.
|
||||||
|
|
||||||
|
13. Update the age of the second hero.
|
||||||
|
|
||||||
|
This updates the object in memory, but not yet in the database.
|
||||||
|
|
||||||
|
14. Add the second hero to the session.
|
||||||
|
|
||||||
|
This puts it in the temporary space in the **session** before committing it to the database.
|
||||||
|
|
||||||
|
15. Commit all the changes tracked in the session.
|
||||||
|
|
||||||
|
This commits everything in one single batch.
|
||||||
|
|
||||||
|
This generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO Engine UPDATE hero SET name=?, age=? WHERE hero.id = ?
|
||||||
|
INFO Engine [generated in 0.00028s] (('Spider-Youngster', 16, 2), ('Captain North America Except Canada', 110, 7))
|
||||||
|
INFO Engine COMMIT
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
See how SQLAlchemy (that powers SQLModel) optimizes the SQL to do as much work as possible in a single batch.
|
||||||
|
|
||||||
|
Here it updates both heroes in a single SQL query.
|
||||||
|
|
||||||
|
16. Refresh the first hero.
|
||||||
|
|
||||||
|
This generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO Engine BEGIN (implicit)
|
||||||
|
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
|
||||||
|
FROM hero
|
||||||
|
WHERE hero.id = ?
|
||||||
|
INFO Engine [generated in 0.00023s] (2,)
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
Because we just committed a SQL transaction with `COMMIT`, SQLAlchemy will automatically start a new transaction with `BEGIN`.
|
||||||
|
|
||||||
|
17. Refresh the second hero.
|
||||||
|
|
||||||
|
This generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
|
||||||
|
FROM hero
|
||||||
|
WHERE hero.id = ?
|
||||||
|
INFO Engine [cached since 0.001709s ago] (7,)
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
SQLAlchemy is still using the previous transaction, so it doesn't have to create a new one.
|
||||||
|
|
||||||
|
18. Print the first hero, now udpated.
|
||||||
|
|
||||||
|
This generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Updated hero 1: name='Spider-Youngster' secret_name='Pedro Parqueador' age=16 id=2
|
||||||
|
```
|
||||||
|
|
||||||
|
19. Print the second hero, now updated.
|
||||||
|
|
||||||
|
This generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Updated hero 2: name='Captain North America Except Canada' secret_name='Esteban Rogelios' age=110 id=7
|
||||||
|
```
|
||||||
|
|
||||||
|
20. Here is the end of the `with` block statement, so the session can execute its terminating code.
|
||||||
|
|
||||||
|
The session will `ROLLBACK` (undo) any possible changes in the last transaction that were not committed.
|
||||||
|
|
||||||
|
This generates the output:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO Engine ROLLBACK
|
||||||
|
```
|
Loading…
Reference in New Issue
Block a user