Skip to content

Commit eb3ca5e

Browse files
committed
📝 docs: add migrations section
1 parent 5fa9062 commit eb3ca5e

File tree

16 files changed

+700
-0
lines changed

16 files changed

+700
-0
lines changed

docs/advanced/migrations.md

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
# Migrations
2+
3+
We will use `Alembic` to handle database schema changes.
4+
5+
`SQLModel` is compatible with `Alembic`.
6+
7+
## Initial example
8+
9+
We'll continue from another example that has the creation of database and tables, and other essentials features.
10+
11+
<details>
12+
<summary>👀 Full file example</summary>
13+
14+
```Python
15+
{!./docs_src/advanced/migrations/tutorial001.py!}
16+
```
17+
18+
</details>
19+
20+
## First step
21+
22+
Add `Alembic` to your project.
23+
24+
Example using pip.
25+
26+
<div class="termy">
27+
28+
```console
29+
$ pip install alembic
30+
31+
Installing collected packages: alembic
32+
Successfully installed alembic-1.8.1
33+
```
34+
35+
</div>
36+
37+
## Clean your code
38+
39+
We need to clean our step that create the database and tables.
40+
41+
```Python hl_lines="3-4"
42+
# Code above omitted 👆
43+
44+
{!./docs_src/advanced/migrations/tutorial001.py[ln:19-20]!}
45+
46+
# Code below omitted 👇
47+
```
48+
49+
```Python hl_lines="4-4"
50+
# Code above omitted 👆
51+
52+
{!./docs_src/advanced/migrations/tutorial001.py[ln:44-47]!}
53+
54+
# Code below omitted 👇
55+
```
56+
57+
<details>
58+
<summary>👀 Full file example</summary>
59+
60+
```Python
61+
{!./docs_src/advanced/migrations/main.py!}
62+
```
63+
64+
</details>
65+
66+
## Alembic configuration
67+
68+
In this step we need initialize alembic.
69+
70+
<div class="termy">
71+
72+
```console
73+
$ alembic init migrations
74+
75+
Creating directory migrations ... done
76+
Creating directory migrations\versions ... done
77+
Generating alembic.ini ... done
78+
Generating migrations\env.py ... done
79+
Generating migrations\README ... done
80+
Generating migrations\script.py.mako ... done
81+
Please edit configuration/connection/logging settings in 'alembic.ini' before proceeding.
82+
83+
```
84+
85+
</div>
86+
87+
!!! info
88+
We can also use `alembic init alembic` to create `alembic` folder instead of `migrations` folder.
89+
90+
Then go to `migrations\script.py.mako` to add sqlmodel module.
91+
92+
```Python hl_lines="5-5"
93+
# Code above omitted 👆
94+
95+
{!./docs_src/advanced/migrations/tutorial003.py[ln:8-10]!}
96+
97+
# Code below omitted 👇
98+
```
99+
100+
!!! info
101+
In new migrations alembic will add SQLModel automatically.
102+
103+
<details>
104+
<summary>👀 Full script.py.mako example</summary>
105+
106+
```Python
107+
{!./docs_src/advanced/migrations/tutorial003.py!}
108+
```
109+
110+
</details>
111+
112+
Then go to `migrations\env.py` to finish the alembic configuration.
113+
114+
- Import your models (in this case `Hero`) and `SQLModel`
115+
116+
```Python hl_lines="5-6"
117+
# Code above omitted 👆
118+
119+
{!./docs_src/advanced/migrations/tutorial004.py[ln:1-6]!}
120+
121+
# Code below omitted 👇
122+
```
123+
124+
!!! warning
125+
First import your models and then import SQLModel otherwise sqlmodel doesn´t recognize all models.
126+
127+
- Then set your database url
128+
129+
```Python hl_lines="4-4"
130+
# Code above omitted 👆
131+
132+
{!./docs_src/advanced/migrations/tutorial004.py[ln:13-14]!}
133+
134+
# Code below omitted 👇
135+
```
136+
137+
!!! tip
138+
This step can be replaced setting the same `sqlalchemy.url` variable in `alembic.ini` file.
139+
140+
- Finally set `target_metadata` with your `SQLModel.metada`
141+
142+
```Python hl_lines="3-3"
143+
# Code above omitted 👆
144+
145+
{!./docs_src/advanced/migrations/tutorial004.py[ln:25-25]!}
146+
147+
# Code below omitted 👇
148+
```
149+
150+
<details>
151+
<summary>👀 Full env.py example</summary>
152+
153+
```Python
154+
{!./docs_src/advanced/migrations/tutorial004.py!}
155+
```
156+
157+
</details>
158+
159+
## Run migrations
160+
161+
In this step we need to generate the initial version of the database.
162+
163+
<div class="termy">
164+
165+
```console
166+
$ alembic revision --autogenerate -m "init_db"
167+
168+
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
169+
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
170+
INFO [alembic.autogenerate.compare] Detected added table 'hero'
171+
Generating migrations\versions\34abfb7ac266_init_db.py ... done
172+
```
173+
174+
</div>
175+
176+
Now in `versions` folder we have a new file called `34abfb7ac266_init_db.py`
177+
178+
!!! info
179+
This file has a revision id and the message part from our revision command.
180+
181+
```{ .python .annotate }
182+
{!./docs_src/advanced/migrations/tutorial005.py!}
183+
```
184+
185+
{!./docs_src/advanced/migrations/annotations/en/tutorial005.md!}
186+
187+
!!! success
188+
At this moment we have all the files to create our new database model.
189+
190+
Initialize the database:
191+
192+
<div class="termy">
193+
194+
```console
195+
$ alembic upgrade head
196+
197+
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
198+
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
199+
INFO [alembic.runtime.migration] Running upgrade -> 34abfb7ac266, init_db
200+
```
201+
202+
</div>
203+
204+
Now we have two tables:
205+
206+
- `alembic_version`: with the version_num asociate with the revision id
207+
- `hero`: the new table from our model
208+
209+
<img class="shadow" src="/img/advanced/migrations/migrations001.png">
210+
211+
`Hero` table is empty.
212+
213+
<img class="shadow" src="/img/advanced/migrations/migrations002.png">
214+
215+
Then run `main.py` script
216+
217+
<div class="termy">
218+
219+
```console
220+
$ python main.py
221+
222+
INFO sqlalchemy.engine.Engine BEGIN (implicit)
223+
INFO sqlalchemy.engine.Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?)
224+
INFO sqlalchemy.engine.Engine [generated in 0.00035s] ('Deadpond', 'Dive Wilson', None)
225+
INFO sqlalchemy.engine.Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?)
226+
INFO sqlalchemy.engine.Engine [cached since 0.002439s ago] ('Spider-Boy', 'Pedro Parqueador', None)
227+
INFO sqlalchemy.engine.Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?)
228+
INFO sqlalchemy.engine.Engine [cached since 0.003134s ago] ('Rusty-Man', 'Tommy Sharp', 48)
229+
INFO sqlalchemy.engine.Engine COMMIT
230+
INFO sqlalchemy.engine.Engine BEGIN (implicit)
231+
INFO sqlalchemy.engine.Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
232+
FROM hero
233+
INFO sqlalchemy.engine.Engine [generated in 0.00038s] ()
234+
age=None id=1 name='Deadpond' secret_name='Dive Wilson'
235+
age=None id=2 name='Spider-Boy' secret_name='Pedro Parqueador'
236+
age=48 id=3 name='Rusty-Man' secret_name='Tommy Sharp'
237+
INFO sqlalchemy.engine.Engine ROLLBACK
238+
```
239+
240+
</div>
241+
242+
Now the `hero` table has new rows:
243+
244+
<img class="shadow" src="/img/advanced/migrations/migrations003.png">
245+
246+
## Next steps
247+
248+
If we edit our model changing the database schema we can run again alembic to generate a new revision.
249+
250+
Example: adding a new field named `power`
251+
252+
```Python hl_lines="4-4"
253+
# Code above omitted 👆
254+
255+
{!./docs_src/advanced/migrations/tutorial006.py[ln:10-11]!}
256+
257+
# Code below omitted 👇
258+
```
259+
260+
<details>
261+
<summary>👀 Full file example</summary>
262+
263+
```Python
264+
{!./docs_src/advanced/migrations/tutorial006.py!}
265+
```
266+
267+
</details>
268+
269+
<div class="termy">
270+
271+
```console
272+
$ alembic revision --autogenerate -m "new field power"
273+
274+
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
275+
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
276+
INFO [alembic.autogenerate.compare] Detected added column 'hero.power'
277+
Generating migrations\versions\b39b8d3c77f0_new_field_power.py ... done
278+
```
279+
280+
</div>
281+
282+
The new file `b39b8d3c77f0_new_field_power.py`:
283+
284+
```{ .python .annotate }
285+
{!./docs_src/advanced/migrations/tutorial007.py!}
286+
```
287+
288+
{!./docs_src/advanced/migrations/annotations/en/tutorial007.md!}
289+
290+
!!! note
291+
Run `alembic upgrade head` to add the new field named power
292+
293+
<div class="termy">
294+
295+
```console
296+
$ alembic upgrade head
297+
298+
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
299+
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
300+
INFO [alembic.runtime.migration] Running upgrade 357d6ebcfadf -> b39b8d3c77f0, new field power
301+
```
302+
303+
</div>
304+
305+
!!! note
306+
After you can downgrade the database to the previous version, run `alembic downgrade -1`
307+
308+
<div class="termy">
309+
310+
```console
311+
$ alembic downgrade -1
312+
313+
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
314+
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
315+
INFO [alembic.runtime.migration] Running downgrade b39b8d3c77f0 -> 357d6ebcfadf, new field power
316+
```
317+
318+
</div>
319+
320+
!!! success
321+
Migrations complete!!! Try adding new tables and relationship.
44.9 KB
Loading
30 KB
Loading
31.2 KB
Loading

docs_src/advanced/migrations/__init__.py

Whitespace-only changes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
1. SQLmodel import
2+
2. Upgrade function to add the new schema in the database
3+
3. Create a new table named `hero`
4+
4. `id` field
5+
5. `name` field
6+
6. `secret_name` field
7+
7. `age` field
8+
8. Setting `id` field as primary key
9+
9. Downgrade function to rollback our changes
10+
10. Delete the table named `hero`
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
1. New revision id
2+
2. Previous revision id, if downgrade go to this revision id
3+
3. `power` new field
4+
4. Drop column if downgrade
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from typing import Optional
2+
3+
from sqlmodel import Field, Session, SQLModel, create_engine, select
4+
5+
6+
class Hero(SQLModel, table=True):
7+
id: Optional[int] = Field(default=None, primary_key=True)
8+
name: str
9+
secret_name: str
10+
age: Optional[int] = None
11+
12+
13+
sqlite_file_name = "database.db"
14+
sqlite_url = f"sqlite:///{sqlite_file_name}"
15+
16+
engine = create_engine(sqlite_url, echo=True)
17+
18+
def create_heroes():
19+
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
20+
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
21+
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
22+
23+
with Session(engine) as session:
24+
session.add(hero_1)
25+
session.add(hero_2)
26+
session.add(hero_3)
27+
28+
session.commit()
29+
30+
31+
def select_heroes():
32+
with Session(engine) as session:
33+
statement = select(Hero)
34+
results = session.exec(statement)
35+
for hero in results:
36+
print(hero)
37+
38+
39+
def main():
40+
create_heroes()
41+
select_heroes()
42+
43+
44+
if __name__ == "__main__":
45+
main()

0 commit comments

Comments
 (0)