46

I am trying to use Alembic for the first time and want to use --autogenerate feature described here

My project structure looks like

project/
       configuration/
                    __init__.py
                    dev.py
                    test.py
       core/
           app/
              models/
                    __init__.py
                    user.py
       db/
          alembic/
                  versions/
                  env.py
          alembic.ini

I am using Flask and SQLAlchemy and their Flask-SQLAlchemy extension. my model User looks like

class User(UserMixin, db.Model):
    __tablename__ = 'users'
    # noinspection PyShadowingBuiltins
    uuid = Column('uuid', GUID(), default=uuid.uuid4, primary_key=True,
                  unique=True)
    email = Column('email', String, nullable=False, unique=True)
    _password = Column('password', String, nullable=False)
    created_on = Column('created_on', sa.types.DateTime(timezone=True),
                        default=datetime.utcnow())
    last_login = Column('last_login', sa.types.DateTime(timezone=True),
                        onupdate=datetime.utcnow())

As described here, I modified env.py to look like

from configuration import app

alembic_config = config.get_section(config.config_ini_section)
alembic_config['sqlalchemy.url'] = app.config['SQLALCHEMY_DATABASE_URI']
engine = engine_from_config(
    alembic_config,
            prefix='sqlalchemy.',
            poolclass=pool.NullPool)

and

from configuration import db


target_metadata = db.metadata

where configuration.__init__py looks like

from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
import dev


app = Flask(__name__)
app.config.from_envvar('SETTINGS_PT')
db = SQLAlchemy(app)

Now when I run migration

$alembic revision --autogenerate -m "Added user table"
INFO  [alembic.migration] Context impl PostgresqlImpl.
INFO  [alembic.migration] Will assume transactional DDL.
  Generating /Users/me/IdeaProjects/project/db/alembic/versions/55a9d5
  35d8ae_added_user_table.py...done

but file alembic/versions/55a9d5 has empty upgrade() and downgrade() methods

"""Added user table

Revision ID: 1b62a62eef0d
Revises: None
Create Date: 2013-03-27 06:37:08.314177

"""

# revision identifiers, used by Alembic.
revision = '1b62a62eef0d'
down_revision = None

from alembic import op
import sqlalchemy as sa


def upgrade():
    ### commands auto generated by Alembic - please adjust! ###
    pass
    ### end Alembic commands ###


def downgrade():
    ### commands auto generated by Alembic - please adjust! ###
    pass
    ### end Alembic commands ###

How come it is not able to understand that there is a new User model? Please help

daydreamer
  • 87,243
  • 191
  • 450
  • 722
  • 18
    your alembic env.py has to make sure it has imported that User model so that the Table is present in memory when migrations run. – zzzeek Mar 27 '13 at 19:35
  • 2
    Can be useful: If empty migrations generated after you update column, check this answer http://stackoverflow.com/a/17176843/1339254 – alexche8 Jan 31 '17 at 16:02
  • 1
    Could you try to see [this answer](https://stackoverflow.com/a/70890339/13503720)? – Henshal B Jun 10 '22 at 07:54

8 Answers8

61

As per @zzzeek, after I included the following in my env.py, I was able to work with --autogenerate option

in env.py under run_migrations_online()

from configuration import app
from core.expense.models import user # added my model here

alembic_config = config.get_section(config.config_ini_section)
alembic_config['sqlalchemy.url'] = app.config['SQLALCHEMY_DATABASE_URI']
engine = engine_from_config(
    alembic_config,
    prefix='sqlalchemy.',
    poolclass=pool.NullPool)

then I ran alembic revision --autogenerate -m "Added initial table" and got

def upgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.create_table('users',
    sa.Column('uuid', sa.GUID(), nullable=False),
    sa.Column('email', sa.String(), nullable=False),
    sa.Column('password', sa.String(), nullable=False),
    sa.Column('created_on', sa.DateTime(timezone=True), nullable=True),
    sa.Column('last_login', sa.DateTime(timezone=True), nullable=True),
    sa.PrimaryKeyConstraint('uuid'),
    sa.UniqueConstraint('email'),
    sa.UniqueConstraint('uuid')
    )
    ### end Alembic commands ###

Thank you Michael for all your help!

daydreamer
  • 87,243
  • 191
  • 450
  • 722
  • Ok, it worked for me also. But in other projects I never needed to import my models to `env.py`, just `Base.metadata` itself and it works. I inspect these other projects but I couldn't find any clue about why they work and my current project won't! I've done the same configuration and in my actual project autogenerate only worked when I imported the model itself! – André Carvalho Dec 05 '22 at 02:23
  • 1
    I've found it: in my other projects I have imported the `Base` and the models in a file (__init__.py), and imported `Base` in `env.py` from this `__init__.py` file. – André Carvalho Dec 05 '22 at 02:51
  • @AndréCarvalho Hi. Can you provide more details about how you've gotten this to work, without having to import all the models to `env.py`? Thank you. – tj56 Mar 28 '23 at 18:56
  • @tj56 I have posted a new detailed answer, you can take a look~ – moon548834 May 23 '23 at 03:23
10

I think it's worth pointing out here that I had the same problem in the current version (0.8.4), but the method of setting metadata appears to have become more explicit: In addition to importing a model, you need to also set target_metadata (which is present in env.py but defaults to None).

The documentation suggests importing something they called Base, but it's not clear what exactly that is; importing the DeclarativeBase instance my models inherit from did nothing for me (same result as OP).

The actual comments in the code, though, suggest setting target_metadata using an actual model (ModelNameHere.metadata), which did work for me (using one model's metadata resulted in all of them being detected).

kungphu
  • 4,592
  • 3
  • 28
  • 37
9

Everything was much simplier, just from starting read the doc i solved my issue with auto generating the migrations. Don't forget to change target_metadata from None to Base.metadata in your env.py file.

target_metadata = Base.metadata

along with the import of the all models you want to track in your __init__.py of your module that consist all db models that inherits from Base model. Then you just need to import these Base in your env.py

from app.database.models import Base

that's it!

Wotori Movako
  • 147
  • 1
  • 7
  • 1
    This answer worked, specifically the reason to import `Base` from the model file is because each of the models are inheriting from `Base`. When you import `Base` from the `models` file it gets all the models inheriting because the import automatically evaluates the class definition of each new model and `Base` adds each one to itself, to then be used in the `target_metadata`. If you don't import from `models` then `Base` never has a chance to get associated to each of the real "models" in your application. – Garry Polley May 24 '22 at 19:03
5

I encountered the same problem and I solved it by importing class from file (note file from directory) as following:

from db.db_setup import Base
from db.models.user import User
from db.models.course import Course

And then droped all tables and autogenerated file then run alembic revision --autogenerate again.

  • this worked for me, seems like importing all models helped (since my models were not all defined in a single file. – Cabrera Apr 04 '23 at 00:36
2

If you don't want your flake8 throwing unused import errors, then you can also add the Record to __all__ in your models file.

At the end of users.py

__all__ = Users

More information at - https://docs.python.org/3/tutorial/modules.html#importing-from-a-package

2

Just share in detail(other people have already answered in words though), the way that do not need to import the models one by one in the env.py

models/
    __init__.py
    base.py
    yourmodel1.py
    yourmodel2.py
alembic/
    versions/
    env.py

In __init.py__:

from models.base import Base
from models.yourmodel2 import yourmodel2
from models.yourmodel1 import yourmodel2

In base.py:

from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

In yourmodel1.py:

from models.base import Base

class Yourmodel1(Base):
    # define columns here

Do the same for other model files

In env.py add this:

from models import Base
target_metadata = Base.metadata

Then you can run the auto upgrade ^_^

moon548834
  • 127
  • 1
  • 7
1

In my case problem was i already had target table created in database. After i dropped this table, revision command produced filled upgrade and downgrade functions.

1

If anyone still has this problem for fastAPI on Postgresql database, do not forget to have your server running that is your uvicorn servers should be running. Then just change this line on alembic ini

sqlalchemy.url = driver://user:pass@localhost/dbname

to this

sqlalchemy.url = 

Then you set the sqlalchemy url in the env.py inside the alembic folder using this just below the config = context.config:

db_string = f"postgresql+psycopg2://postgresusername:12345678@localhost:5432/mydatabase"
config.set_main_option("sqlalchemy.url", db_string)

Then import your models for example from db.base_class import Base just before target_metadata = None , and change the target to this:

target_metadata = Base.metadata

So my ```env.py`` looks like this:

from logging.config import fileConfig

from sqlalchemy import engine_from_config
from sqlalchemy import pool
from core.config import settings

from alembic import context

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

db_string = f"postgresql+psycopg2://postgresusername:12345678@localhost:5432/mydatabase"
config.set_main_option("sqlalchemy.url", db_string)

# 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 = None

from db.base_class import Base

target_metadata = Base.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()


pythonGo
  • 125
  • 2
  • 12
  • Why does your api server need to be running? That shouldn't make any difference to connecting to the db right? – echo Jan 11 '23 at 22:07
  • Tried the async version of this in my fastapi project and the migration was still blank – echo Jan 11 '23 at 22:19
  • I really don't know why. But when the servers were not running, my migration wouldn't work. – pythonGo Jan 13 '23 at 04:09