diff --git a/docs/advanced/migrations.md b/docs/advanced/migrations.md new file mode 100644 index 0000000000..acf2a6fd6e --- /dev/null +++ b/docs/advanced/migrations.md @@ -0,0 +1,321 @@ +# Migrations + +We will use `Alembic` to handle database schema changes. + +`SQLModel` is compatible with `Alembic`. + +## Initial example + +We'll continue from another example that has the creation of database and tables, and other essentials features. + +
+πŸ‘€ Full file example + +```Python +{!./docs_src/advanced/migrations/tutorial001.py!} +``` + +
+ +## First step + +Add `Alembic` to your project. + +Example using pip. + +
+ +```console +$ pip install alembic + +Installing collected packages: alembic +Successfully installed alembic-1.8.1 +``` + +
+ +## Clean your code + +We need to clean our step that create the database and tables. + +```Python hl_lines="3-4" +# Code above omitted πŸ‘† + +{!./docs_src/advanced/migrations/tutorial001.py[ln:19-20]!} + +# Code below omitted πŸ‘‡ +``` + +```Python hl_lines="4-4" +# Code above omitted πŸ‘† + +{!./docs_src/advanced/migrations/tutorial001.py[ln:44-47]!} + +# Code below omitted πŸ‘‡ +``` + +
+πŸ‘€ Full file example + +```Python +{!./docs_src/advanced/migrations/main.py!} +``` + +
+ +## Alembic configuration + +In this step we need initialize alembic. + +
+ +```console +$ alembic init migrations + +Creating directory migrations ... done +Creating directory migrations\versions ... done +Generating alembic.ini ... done +Generating migrations\env.py ... done +Generating migrations\README ... done +Generating migrations\script.py.mako ... done +Please edit configuration/connection/logging settings in 'alembic.ini' before proceeding. + +``` + +
+ +!!! info + We can also use `alembic init alembic` to create `alembic` folder instead of `migrations` folder. + +Then go to `migrations\script.py.mako` to add sqlmodel module. + +```Python hl_lines="5-5" +# Code above omitted πŸ‘† + +{!./docs_src/advanced/migrations/tutorial003.mako[ln:8-10]!} + +# Code below omitted πŸ‘‡ +``` + +!!! info + In new migrations alembic will add SQLModel automatically. + +
+πŸ‘€ Full script.py.mako example + +```Python +{!./docs_src/advanced/migrations/tutorial003.mako!} +``` + +
+ +Then go to `migrations\env.py` to finish the alembic configuration. + +- Import your models (in this case `Hero`) and `SQLModel` + +```Python hl_lines="5-6" +# Code above omitted πŸ‘† + +{!./docs_src/advanced/migrations/tutorial004.py[ln:1-6]!} + +# Code below omitted πŸ‘‡ +``` + +!!! warning + First import your models and then import SQLModel otherwise sqlmodel doesnΒ΄t recognize all models. + +- Then set your database url + +```Python hl_lines="4-4" +# Code above omitted πŸ‘† + +{!./docs_src/advanced/migrations/tutorial004.py[ln:13-14]!} + +# Code below omitted πŸ‘‡ +``` + +!!! tip + This step can be replaced setting the same `sqlalchemy.url` variable in `alembic.ini` file. + +- Finally set `target_metadata` with your `SQLModel.metada` + +```Python hl_lines="3-3" +# Code above omitted πŸ‘† + +{!./docs_src/advanced/migrations/tutorial004.py[ln:25-25]!} + +# Code below omitted πŸ‘‡ +``` + +
+πŸ‘€ Full env.py example + +```Python +{!./docs_src/advanced/migrations/tutorial004.py!} +``` + +
+ +## Run migrations + +In this step we need to generate the initial version of the database. + +
+ +```console +$ alembic revision --autogenerate -m "init_db" + +INFO [alembic.runtime.migration] Context impl SQLiteImpl. +INFO [alembic.runtime.migration] Will assume non-transactional DDL. +INFO [alembic.autogenerate.compare] Detected added table 'hero' +Generating migrations\versions\34abfb7ac266_init_db.py ... done +``` + +
+ +Now in `versions` folder we have a new file called `34abfb7ac266_init_db.py` + +!!! info + This file has a revision id and the message part from our revision command. + +```{ .python .annotate } +{!./docs_src/advanced/migrations/tutorial005.py!} +``` + +{!./docs_src/advanced/migrations/annotations/en/tutorial005.md!} + +!!! success + At this moment we have all the files to create our new database model. + +Initialize the database: + +
+ +```console +$ alembic upgrade head + +INFO [alembic.runtime.migration] Context impl SQLiteImpl. +INFO [alembic.runtime.migration] Will assume non-transactional DDL. +INFO [alembic.runtime.migration] Running upgrade -> 34abfb7ac266, init_db +``` + +
+ +Now we have two tables: + +- `alembic_version`: with the version_num asociate with the revision id +- `hero`: the new table from our model + + + +`Hero` table is empty. + + + +Then run `main.py` script + +
+ +```console +$ python main.py + +INFO sqlalchemy.engine.Engine BEGIN (implicit) +INFO sqlalchemy.engine.Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?) +INFO sqlalchemy.engine.Engine [generated in 0.00035s] ('Deadpond', 'Dive Wilson', None) +INFO sqlalchemy.engine.Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?) +INFO sqlalchemy.engine.Engine [cached since 0.002439s ago] ('Spider-Boy', 'Pedro Parqueador', None) +INFO sqlalchemy.engine.Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?) +INFO sqlalchemy.engine.Engine [cached since 0.003134s ago] ('Rusty-Man', 'Tommy Sharp', 48) +INFO sqlalchemy.engine.Engine COMMIT +INFO sqlalchemy.engine.Engine BEGIN (implicit) +INFO sqlalchemy.engine.Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero +INFO sqlalchemy.engine.Engine [generated in 0.00038s] () +age=None id=1 name='Deadpond' secret_name='Dive Wilson' +age=None id=2 name='Spider-Boy' secret_name='Pedro Parqueador' +age=48 id=3 name='Rusty-Man' secret_name='Tommy Sharp' +INFO sqlalchemy.engine.Engine ROLLBACK +``` + +
+ +Now the `hero` table has new rows: + + + +## Next steps + +If we edit our model changing the database schema we can run again alembic to generate a new revision. + +Example: adding a new field named `power` + +```Python hl_lines="4-4" +# Code above omitted πŸ‘† + +{!./docs_src/advanced/migrations/tutorial006.py[ln:10-11]!} + +# Code below omitted πŸ‘‡ +``` + +
+πŸ‘€ Full file example + +```Python +{!./docs_src/advanced/migrations/tutorial006.py!} +``` + +
+ +
+ +```console +$ alembic revision --autogenerate -m "new field power" + +INFO [alembic.runtime.migration] Context impl SQLiteImpl. +INFO [alembic.runtime.migration] Will assume non-transactional DDL. +INFO [alembic.autogenerate.compare] Detected added column 'hero.power' +Generating migrations\versions\b39b8d3c77f0_new_field_power.py ... done +``` + +
+ +The new file `b39b8d3c77f0_new_field_power.py`: + +```{ .python .annotate } +{!./docs_src/advanced/migrations/tutorial007.py!} +``` + +{!./docs_src/advanced/migrations/annotations/en/tutorial007.md!} + +!!! note + Run `alembic upgrade head` to add the new field named power + +
+ +```console +$ alembic upgrade head + +INFO [alembic.runtime.migration] Context impl SQLiteImpl. +INFO [alembic.runtime.migration] Will assume non-transactional DDL. +INFO [alembic.runtime.migration] Running upgrade 357d6ebcfadf -> b39b8d3c77f0, new field power +``` + +
+ +!!! note + After you can downgrade the database to the previous version, run `alembic downgrade -1` + +
+ +```console +$ alembic downgrade -1 + +INFO [alembic.runtime.migration] Context impl SQLiteImpl. +INFO [alembic.runtime.migration] Will assume non-transactional DDL. +INFO [alembic.runtime.migration] Running downgrade b39b8d3c77f0 -> 357d6ebcfadf, new field power +``` + +
+ +!!! success + Migrations complete!!! Try adding new tables and relationship. diff --git a/docs/img/advanced/migrations/migrations001.png b/docs/img/advanced/migrations/migrations001.png new file mode 100644 index 0000000000..d439f416c9 Binary files /dev/null and b/docs/img/advanced/migrations/migrations001.png differ diff --git a/docs/img/advanced/migrations/migrations002.png b/docs/img/advanced/migrations/migrations002.png new file mode 100644 index 0000000000..e3eb43155e Binary files /dev/null and b/docs/img/advanced/migrations/migrations002.png differ diff --git a/docs/img/advanced/migrations/migrations003.png b/docs/img/advanced/migrations/migrations003.png new file mode 100644 index 0000000000..f86db2735d Binary files /dev/null and b/docs/img/advanced/migrations/migrations003.png differ diff --git a/docs_src/advanced/migrations/__init__.py b/docs_src/advanced/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/advanced/migrations/annotations/en/tutorial005.md b/docs_src/advanced/migrations/annotations/en/tutorial005.md new file mode 100644 index 0000000000..a3408469f2 --- /dev/null +++ b/docs_src/advanced/migrations/annotations/en/tutorial005.md @@ -0,0 +1,10 @@ +1. SQLmodel import +2. Upgrade function to add the new schema in the database +3. Create a new table named `hero` +4. `id` field +5. `name` field +6. `secret_name` field +7. `age` field +8. Setting `id` field as primary key +9. Downgrade function to rollback our changes +10. Delete the table named `hero` diff --git a/docs_src/advanced/migrations/annotations/en/tutorial007.md b/docs_src/advanced/migrations/annotations/en/tutorial007.md new file mode 100644 index 0000000000..21970a79bd --- /dev/null +++ b/docs_src/advanced/migrations/annotations/en/tutorial007.md @@ -0,0 +1,4 @@ +1. New revision id +2. Previous revision id, if downgrade go to this revision id +3. `power` new field +4. Drop column if downgrade diff --git a/docs_src/advanced/migrations/main.py b/docs_src/advanced/migrations/main.py new file mode 100644 index 0000000000..067a8292f9 --- /dev/null +++ b/docs_src/advanced/migrations/main.py @@ -0,0 +1,46 @@ +from typing import Optional + +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + secret_name: str + age: Optional[int] = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/advanced/migrations/tutorial001.py b/docs_src/advanced/migrations/tutorial001.py new file mode 100644 index 0000000000..285f711221 --- /dev/null +++ b/docs_src/advanced/migrations/tutorial001.py @@ -0,0 +1,51 @@ +from typing import Optional + +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + secret_name: str + age: Optional[int] = None + + +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_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/advanced/migrations/tutorial002.py b/docs_src/advanced/migrations/tutorial002.py new file mode 100644 index 0000000000..3aa2e2b09f --- /dev/null +++ b/docs_src/advanced/migrations/tutorial002.py @@ -0,0 +1,51 @@ +from typing import Optional + +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + secret_name: str + age: Optional[int] = None + + +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_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + # create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/advanced/migrations/tutorial003.mako b/docs_src/advanced/migrations/tutorial003.mako new file mode 100644 index 0000000000..3124b62c2a --- /dev/null +++ b/docs_src/advanced/migrations/tutorial003.mako @@ -0,0 +1,25 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/docs_src/advanced/migrations/tutorial004.py b/docs_src/advanced/migrations/tutorial004.py new file mode 100644 index 0000000000..e113d5c650 --- /dev/null +++ b/docs_src/advanced/migrations/tutorial004.py @@ -0,0 +1,75 @@ +from logging.config import fileConfig # isort:skip + +from main import Hero # noqa: F401, isort:skip +from sqlmodel import SQLModel # isort:skip + +from sqlalchemy import engine_from_config # isort:skip +from sqlalchemy import pool # isort:skip + +from alembic import context # isort:skip + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config +config.set_main_option("sqlalchemy.url", "sqlite:///database.db") + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = SQLModel.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + Calls to context.execute() here emit the given string to the + script output. + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + In this scenario we need to create an Engine + and associate a connection with the context. + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/docs_src/advanced/migrations/tutorial005.py b/docs_src/advanced/migrations/tutorial005.py new file mode 100644 index 0000000000..bd5d529bd9 --- /dev/null +++ b/docs_src/advanced/migrations/tutorial005.py @@ -0,0 +1,38 @@ +"""init_db + +Revision ID: 34abfb7ac266 +Revises: +Create Date: + +""" +from alembic import op # isort:skip +import sqlalchemy as sa # isort:skip +import sqlmodel # (1), # isort:skip + + +# revision identifiers, used by Alembic. +revision = "34abfb7ac266" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade() -> None: # (2) + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "hero", # (3) + sa.Column("id", sa.Integer(), nullable=False), # (4) + sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), # (5) + sa.Column( + "secret_name", sqlmodel.sql.sqltypes.AutoString(), nullable=False + ), # (6) + sa.Column("age", sa.Integer(), nullable=True), # (7) + sa.PrimaryKeyConstraint("id"), # (8) + ) + # ### end Alembic commands ### + + +def downgrade() -> None: # (9) + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("hero") # (10) + # ### end Alembic commands ### diff --git a/docs_src/advanced/migrations/tutorial006.py b/docs_src/advanced/migrations/tutorial006.py new file mode 100644 index 0000000000..8470140a52 --- /dev/null +++ b/docs_src/advanced/migrations/tutorial006.py @@ -0,0 +1,47 @@ +from typing import Optional + +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + secret_name: str + age: Optional[int] = None + power: int = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/advanced/migrations/tutorial007.py b/docs_src/advanced/migrations/tutorial007.py new file mode 100644 index 0000000000..0f332b9000 --- /dev/null +++ b/docs_src/advanced/migrations/tutorial007.py @@ -0,0 +1,29 @@ +"""new field power + +Revision ID: b39b8d3c77f0 +Revises: 357d6ebcfadf +Create Date: + +""" +from alembic import op # isort:skip +import sqlalchemy as sa # isort:skip +import sqlmodel # noqa: F401, isort:skip + + +# revision identifiers, used by Alembic. +revision = "b39b8d3c77f0" # (1) +down_revision = "357d6ebcfadf" # (2) +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("hero", sa.Column("power", sa.Integer(), nullable=True)) # (3) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("hero", "power") # (4) + # ### end Alembic commands ### diff --git a/mkdocs.yml b/mkdocs.yml index a27bbde8a1..237fa3aa69 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -85,6 +85,7 @@ nav: - Advanced User Guide: - advanced/index.md - advanced/decimal.md + - advanced/migrations.md - alternatives.md - help.md - contributing.md