From ffce0d74386e2e8d62e021dd422115cf7b14785f Mon Sep 17 00:00:00 2001 From: pctablet505 <55033230+pctablet505@users.noreply.github.com> Date: Wed, 15 Jul 2026 12:53:53 +0000 Subject: [PATCH 1/3] docs: add complete alembic integration example Expand the migrations/Alembic docs with a working, end-to-end example: - Explain where the SQLAlchemy MetaData comes from in ormar and how to expose it for Alembic. - Add a sample project layout with my_project/models.py, alembic.ini, and alembic/env.py. - Show how to handle models split across multiple apps/packages by importing all models before Alembic reads the metadata. - Keep the existing compare_type and include_object tips intact. Fixes #1400 --- docs/models/migrations.md | 304 ++++++++++++++++++++++++++++++-------- 1 file changed, 241 insertions(+), 63 deletions(-) diff --git a/docs/models/migrations.md b/docs/models/migrations.md index ace22001d..f965b3257 100644 --- a/docs/models/migrations.md +++ b/docs/models/migrations.md @@ -38,102 +38,181 @@ Likewise as with tables, since we base tables on sqlalchemy for migrations pleas Use command line to reproduce this minimalistic example. -```python +```bash alembic init alembic alembic revision --autogenerate -m "made some changes" alembic upgrade head ``` -### Sample env.py file +### Where does `metadata` come from? + +Ormar models are built on SQLAlchemy Core. Each model stores its table on a SQLAlchemy `MetaData` instance that you provide through `OrmarConfig`. Alembic needs that same `MetaData` object so it can discover which tables exist and autogenerate migrations. -A quick example of alembic migrations should be something similar to: +The usual pattern is to create one shared `MetaData` object and pass it to every model: -When you have application structure like: +```python +# my_project/models.py +import sqlalchemy +import ormar +from ormar import DatabaseConnection +database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite") +metadata = sqlalchemy.MetaData() + + +class Author(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="authors", + ) + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + + +class Book(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="books", + ) + + id: int = ormar.Integer(primary_key=True) + title: str = ormar.String(max_length=100) + author: Author = ormar.ForeignKey(Author) ``` --> app - -> alembic (initialized folder - so run alembic init alembic inside app folder) - -> models (here are the models) - -> __init__.py - -> my_models.py + +You can then expose `metadata` from the module that defines your models: + +```python +# env.py +from my_project.models import metadata + +target_metadata = metadata ``` -Your `env.py` file (in alembic folder) can look something like: +If you prefer, you can also grab the metadata from any model: ```python -from logging.config import fileConfig -from sqlalchemy import create_engine +target_metadata = Author.ormar_config.metadata +``` -from alembic import context -import sys, os +The important part is that every model uses the **same** `sqlalchemy.MetaData()` instance. If models live in different files or apps, import them all before Alembic inspects the metadata (see [Multiple apps with models](#multiple-apps-with-models) below). -# add app folder to system path (alternative is running it from parent folder with python -m ...) -myPath = os.path.dirname(os.path.abspath(__file__)) -sys.path.insert(0, myPath + '/../../') +### Complete project layout -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config +Below is a minimal but complete layout that works with `alembic revision --autogenerate`. -# Interpret the config file for Python logging. -# This line sets up loggers basically. -fileConfig(config.config_file_name) +``` +my_project/ +├── alembic/ +│ ├── env.py +│ ├── script.py.mako +│ └── versions/ +├── my_project/ +│ ├── __init__.py +│ └── models.py +├── alembic.ini +└── db.sqlite +``` -# add your model's MetaData object here (the one used in ormar) -# for 'autogenerate' support -from app.models.my_models import metadata -target_metadata = metadata +`my_project/models.py`: +```python +import sqlalchemy +import ormar +from ormar import DatabaseConnection -# set your url here or import from settings -# note that by default url is in saved sqlachemy.url variable in alembic.ini file -URL = "sqlite:///test.db" +database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite") +metadata = sqlalchemy.MetaData() -def run_migrations_offline(): - """Run migrations in 'offline' mode. +class Author(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="authors", + ) + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + + +class Book(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="books", + ) + + id: int = ormar.Integer(primary_key=True) + title: str = ormar.String(max_length=100) + author: Author = ormar.ForeignKey(Author) +``` - 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. +`alembic.ini` (only the parts you typically need to change): - Calls to context.execute() here emit the given string to the - script output. +```ini +[alembic] +script_location = %(here)s/alembic - """ +prepend_sys_path = . + +sqlalchemy.url = sqlite:///db.sqlite +``` + +`alembic/env.py`: + +```python +from logging.config import fileConfig +from pathlib import Path +import sys + +from sqlalchemy import engine_from_config, pool +from alembic import context + +# Add the project root to sys.path so `my_project` can be imported. +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +config = context.config +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# Import the shared metadata (and models, so the metaclass registers the tables). +from my_project.models import metadata + +target_metadata = metadata + + +def run_migrations_offline() -> None: + url = config.get_main_option("sqlalchemy.url") context.configure( - url=URL, + url=url, target_metadata=target_metadata, literal_binds=True, dialect_opts={"paramstyle": "named"}, - # if you use UUID field set also this param - # the prefix has to match sqlalchemy import name in alembic - # that can be set by sqlalchemy_module_prefix option (default 'sa.') - user_module_prefix='sa.' + # Required if you use ormar.UUID(). + user_module_prefix="sa.", ) with context.begin_transaction(): context.run_migrations() -def run_migrations_online(): - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - connectable = create_engine(URL) +def run_migrations_online() -> None: + 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, - # if you use UUID field set also this param - # the prefix has to match sqlalchemy import name in alembic - # that can be set by sqlalchemy_module_prefix option (default 'sa.') - user_module_prefix='sa.' + # Required if you use ormar.UUID(). + user_module_prefix="sa.", ) with context.begin_transaction(): @@ -144,9 +223,108 @@ if context.is_offline_mode(): run_migrations_offline() else: run_migrations_online() +``` + +Then generate and run your first migration: + +```bash +alembic revision --autogenerate -m "initial" +alembic upgrade head +``` + +### Multiple apps with models + +If your models are split across several packages, the metadata must still be shared and every model must be imported before Alembic reads the metadata. A common layout: + +``` +my_project/ +├── alembic/ +│ └── env.py +├── my_project/ +│ ├── __init__.py +│ ├── database.py +│ ├── models/ +│ │ ├── __init__.py +│ │ ├── authors.py +│ │ └── books.py +│ ├── authors/ +│ │ └── models.py +│ └── books/ +│ └── models.py +└── alembic.ini +``` + +`my_project/database.py`: + +```python +import sqlalchemy +from ormar import DatabaseConnection + +database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite") +metadata = sqlalchemy.MetaData() +``` + +`my_project/authors/models.py`: + +```python +import ormar +from my_project.database import database, metadata + + +class Author(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="authors", + ) + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) +``` +`my_project/books/models.py`: + +```python +import ormar +from my_project.authors.models import Author +from my_project.database import database, metadata + + +class Book(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="books", + ) + + id: int = ormar.Integer(primary_key=True) + title: str = ormar.String(max_length=100) + author: Author = ormar.ForeignKey(Author) ``` +`my_project/models/__init__.py` acts as a central import point: + +```python +# Import every model so the metaclass registers its table on the shared metadata. +from my_project.authors.models import Author +from my_project.books.models import Book + +# Re-export the shared metadata so env.py can import it from one place. +from my_project.database import metadata + +__all__ = ["Author", "Book", "metadata"] +``` + +Then in `alembic/env.py`: + +```python +from my_project.models import metadata, Author, Book # noqa: F401 + +target_metadata = metadata +``` + +Importing the models is required: if a model class is never defined/imported, its table is never attached to `metadata` and Alembic will not see it. + ### Detecting column type changes (`compare_type`) Alembic's `--autogenerate` does **not** compare column types by default — it only @@ -196,13 +374,13 @@ def include_object(object, name, type_, reflected, compare_to): And you pass it into context like (both in online and offline): ```python context.configure( - url=URL, - target_metadata=target_metadata, - literal_binds=True, - dialect_opts={"paramstyle": "named"}, - user_module_prefix='sa.', - include_object=include_object - ) + url=config.get_main_option("sqlalchemy.url"), + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + user_module_prefix="sa.", + include_object=include_object, +) ``` !!!info From 373ca1dae6b1e0ace043b191e7228ad919f26159 Mon Sep 17 00:00:00 2001 From: pctablet505 <55033230+pctablet505@users.noreply.github.com> Date: Wed, 15 Jul 2026 12:59:20 +0000 Subject: [PATCH 2/3] docs(migrations): remove stray files from multiple-apps tree --- docs/models/migrations.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/models/migrations.md b/docs/models/migrations.md index f965b3257..45fcb8df8 100644 --- a/docs/models/migrations.md +++ b/docs/models/migrations.md @@ -244,9 +244,7 @@ my_project/ │ ├── __init__.py │ ├── database.py │ ├── models/ -│ │ ├── __init__.py -│ │ ├── authors.py -│ │ └── books.py +│ │ └── __init__.py │ ├── authors/ │ │ └── models.py │ └── books/ From bcf42e23705e1bbc83f8ef036f5c75e429c8c96f Mon Sep 17 00:00:00 2001 From: pctablet505 <55033230+pctablet505@users.noreply.github.com> Date: Thu, 3 Sep 2026 11:20:34 +0530 Subject: [PATCH 3/3] docs(migrations): address all review comments on alembic config, logging, and package layouts - Trim duplicate model definitions in 'Where does metadata come from?' - Extract runnable models to docs_src/models/docs019.py via snippet syntax - Include standard logging sections in alembic.ini to prevent KeyError: 'formatters' - Add callout and driver comparison table explaining async vs sync URLs and alembic init -t async - Drop redundant prepend_sys_path from alembic.ini (env.py handles sys.path) - Pin db.sqlite path in alembic.ini using %(here)s to avoid cwd dependency - Add missing __init__.py files in multi-app package layout - Add noqa: E402 annotations to env.py snippets after sys.path manipulation - Retain explanatory comment on user_module_prefix matching sqlalchemy_module_prefix and normalize quotes --- docs/models/migrations.md | 136 ++++++++++++++++--------------------- docs_src/models/docs019.py | 29 ++++++++ 2 files changed, 86 insertions(+), 79 deletions(-) create mode 100644 docs_src/models/docs019.py diff --git a/docs/models/migrations.md b/docs/models/migrations.md index 45fcb8df8..99a381c13 100644 --- a/docs/models/migrations.md +++ b/docs/models/migrations.md @@ -48,56 +48,14 @@ alembic upgrade head Ormar models are built on SQLAlchemy Core. Each model stores its table on a SQLAlchemy `MetaData` instance that you provide through `OrmarConfig`. Alembic needs that same `MetaData` object so it can discover which tables exist and autogenerate migrations. -The usual pattern is to create one shared `MetaData` object and pass it to every model: - -```python -# my_project/models.py -import sqlalchemy -import ormar -from ormar import DatabaseConnection - -database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite") -metadata = sqlalchemy.MetaData() - - -class Author(ormar.Model): - ormar_config = ormar.OrmarConfig( - database=database, - metadata=metadata, - tablename="authors", - ) - - id: int = ormar.Integer(primary_key=True) - name: str = ormar.String(max_length=100) - - -class Book(ormar.Model): - ormar_config = ormar.OrmarConfig( - database=database, - metadata=metadata, - tablename="books", - ) - - id: int = ormar.Integer(primary_key=True) - title: str = ormar.String(max_length=100) - author: Author = ormar.ForeignKey(Author) -``` - -You can then expose `metadata` from the module that defines your models: - -```python -# env.py -from my_project.models import metadata - -target_metadata = metadata -``` - -If you prefer, you can also grab the metadata from any model: +The usual pattern is to create one shared `MetaData` object and pass it to every model in its `OrmarConfig`. You can then expose that metadata from any model: ```python target_metadata = Author.ormar_config.metadata ``` +or export the shared `metadata` instance from your models module and import it into `env.py`. + The important part is that every model uses the **same** `sqlalchemy.MetaData()` instance. If models live in different files or apps, import them all before Alembic inspects the metadata (see [Multiple apps with models](#multiple-apps-with-models) below). ### Complete project layout @@ -120,48 +78,64 @@ my_project/ `my_project/models.py`: ```python -import sqlalchemy -import ormar -from ormar import DatabaseConnection +--8<-- "../docs_src/models/docs019.py" +``` -database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite") -metadata = sqlalchemy.MetaData() +`alembic.ini`: +```ini +[alembic] +script_location = %(here)s/alembic -class Author(ormar.Model): - ormar_config = ormar.OrmarConfig( - database=database, - metadata=metadata, - tablename="authors", - ) +sqlalchemy.url = sqlite:///%(here)s/db.sqlite - id: int = ormar.Integer(primary_key=True) - name: str = ormar.String(max_length=100) +# Logging configuration (required by fileConfig in env.py) +[loggers] +keys = root,sqlalchemy,alembic +[handlers] +keys = console -class Book(ormar.Model): - ormar_config = ormar.OrmarConfig( - database=database, - metadata=metadata, - tablename="books", - ) +[formatters] +keys = generic - id: int = ormar.Integer(primary_key=True) - title: str = ormar.String(max_length=100) - author: Author = ormar.ForeignKey(Author) -``` +[logger_root] +level = WARN +handlers = console +qualname = -`alembic.ini` (only the parts you typically need to change): +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine -```ini -[alembic] -script_location = %(here)s/alembic +[logger_alembic] +level = INFO +handlers = +qualname = alembic -prepend_sys_path = . +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic -sqlalchemy.url = sqlite:///db.sqlite +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S ``` +!!! note "Async vs. sync database drivers" + Ormar communicates with the database using an asynchronous driver (`sqlite+aiosqlite`, `asyncpg`, `aiomysql`), whereas Alembic's default `env.py` runs synchronously. The two connection URLs intentionally point to the same database through different driver protocols: + + | Database | Ormar (`DatabaseConnection`) | Alembic (`sqlalchemy.url`) | + |---|---|---| + | SQLite | `sqlite+aiosqlite:///db.sqlite` | `sqlite:///%(here)s/db.sqlite` | + | PostgreSQL | `postgresql+asyncpg://...` | `postgresql+psycopg2://...` | + | MySQL | `mysql+aiomysql://...` | `mysql+pymysql://...` | + + If you prefer to use async drivers throughout, you can initialize Alembic with `alembic init -t async`. + `alembic/env.py`: ```python @@ -180,7 +154,7 @@ if config.config_file_name is not None: fileConfig(config.config_file_name) # Import the shared metadata (and models, so the metaclass registers the tables). -from my_project.models import metadata +from my_project.models import metadata # noqa: E402 target_metadata = metadata @@ -192,7 +166,8 @@ def run_migrations_offline() -> None: target_metadata=target_metadata, literal_binds=True, dialect_opts={"paramstyle": "named"}, - # Required if you use ormar.UUID(). + # Required if you use ormar.UUID(); must match alembic's + # sqlalchemy_module_prefix option (default "sa."). user_module_prefix="sa.", ) @@ -211,7 +186,8 @@ def run_migrations_online() -> None: context.configure( connection=connection, target_metadata=target_metadata, - # Required if you use ormar.UUID(). + # Required if you use ormar.UUID(); must match alembic's + # sqlalchemy_module_prefix option (default "sa."). user_module_prefix="sa.", ) @@ -246,8 +222,10 @@ my_project/ │ ├── models/ │ │ └── __init__.py │ ├── authors/ +│ │ ├── __init__.py │ │ └── models.py │ └── books/ +│ ├── __init__.py │ └── models.py └── alembic.ini ``` @@ -316,7 +294,7 @@ __all__ = ["Author", "Book", "metadata"] Then in `alembic/env.py`: ```python -from my_project.models import metadata, Author, Book # noqa: F401 +from my_project.models import Author, Book, metadata # noqa: E402, F401 target_metadata = metadata ``` @@ -337,7 +315,7 @@ and `run_migrations_online`: context.configure( connection=connection, target_metadata=target_metadata, - user_module_prefix='sa.', + user_module_prefix="sa.", compare_type=True, ) ``` diff --git a/docs_src/models/docs019.py b/docs_src/models/docs019.py new file mode 100644 index 000000000..14ca82cdb --- /dev/null +++ b/docs_src/models/docs019.py @@ -0,0 +1,29 @@ +import ormar +import sqlalchemy +from ormar import DatabaseConnection + +database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite") +metadata = sqlalchemy.MetaData() + + +class Author(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="authors", + ) + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + + +class Book(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="books", + ) + + id: int = ormar.Integer(primary_key=True) + title: str = ormar.String(max_length=100) + author: Author = ormar.ForeignKey(Author)