Skip to content

Spurious loop detected with 1.5.0+ when revision is substring of down_revision #784

Description

@alext

Describe the bug

Since upgrading alembic from 1.4.3 to 1.5.1 we've been getting spurious "Self-loop is detected errors" from our migrations. This appears to happen for any migration where revision is a substring of down_revision.

Expected behavior

This migrations should continue to be parsed and processed as before.

To Reproduce

I've created a minimal flask-sqlalchemy app with flask-migrate as follows:

app.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql:///minimal_app'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
migrate = Migrate(app, db)


class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)


@app.route('/')
def hello():
    return "Hello World!"

if __name__ == '__main__':
    app.run()

migrations/versions/skeleton_user_.py

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'skeleton_user'
down_revision = None
branch_labels = None
depends_on = None


def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('user',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.PrimaryKeyConstraint('id')
    )
    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('user')
    # ### end Alembic commands ###

migrations/versions/user_.py

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'user'
down_revision = 'skeleton_user'
branch_labels = None
depends_on = None


def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.add_column('user', sa.Column('name', sa.String(), nullable=True))
    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_column('user', 'name')
    # ### end Alembic commands ###

Error

$ flask db heads
Traceback (most recent call last):
  File "/home/alex/.pyenv/versions/minimal-app/bin/flask", line 8, in <module>
    sys.exit(main())
  File "/home/alex/.pyenv/versions/3.8.6/envs/minimal-app/lib/python3.8/site-packages/flask/cli.py", line 967, in main
    cli.main(args=sys.argv[1:], prog_name="python -m flask" if as_module else None)
  File "/home/alex/.pyenv/versions/3.8.6/envs/minimal-app/lib/python3.8/site-packages/flask/cli.py", line 586, in main
    return super(FlaskGroup, self).main(*args, **kwargs)
  File "/home/alex/.pyenv/versions/3.8.6/envs/minimal-app/lib/python3.8/site-packages/click/core.py", line 782, in main
    rv = self.invoke(ctx)
  File "/home/alex/.pyenv/versions/3.8.6/envs/minimal-app/lib/python3.8/site-packages/click/core.py", line 1259, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/home/alex/.pyenv/versions/3.8.6/envs/minimal-app/lib/python3.8/site-packages/click/core.py", line 1259, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/home/alex/.pyenv/versions/3.8.6/envs/minimal-app/lib/python3.8/site-packages/click/core.py", line 1066, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/home/alex/.pyenv/versions/3.8.6/envs/minimal-app/lib/python3.8/site-packages/click/core.py", line 610, in invoke
    return callback(*args, **kwargs)
  File "/home/alex/.pyenv/versions/3.8.6/envs/minimal-app/lib/python3.8/site-packages/click/decorators.py", line 21, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "/home/alex/.pyenv/versions/3.8.6/envs/minimal-app/lib/python3.8/site-packages/flask/cli.py", line 426, in decorator
    return __ctx.invoke(f, *args, **kwargs)
  File "/home/alex/.pyenv/versions/3.8.6/envs/minimal-app/lib/python3.8/site-packages/click/core.py", line 610, in invoke
    return callback(*args, **kwargs)
  File "/home/alex/.pyenv/versions/3.8.6/envs/minimal-app/lib/python3.8/site-packages/flask_migrate/cli.py", line 191, in heads
    _heads(directory, verbose, resolve_dependencies)
  File "/home/alex/.pyenv/versions/3.8.6/envs/minimal-app/lib/python3.8/site-packages/flask_migrate/__init__.py", line 96, in wrapped
    f(*args, **kwargs)
  File "/home/alex/.pyenv/versions/3.8.6/envs/minimal-app/lib/python3.8/site-packages/flask_migrate/__init__.py", line 346, in heads
    command.heads(config, verbose=verbose,
  File "/home/alex/.pyenv/versions/3.8.6/envs/minimal-app/lib/python3.8/site-packages/alembic/command.py", line 442, in heads
    heads = script.get_revisions(script.get_heads())
  File "/home/alex/.pyenv/versions/3.8.6/envs/minimal-app/lib/python3.8/site-packages/alembic/script/base.py", line 314, in get_heads
    return list(self.revision_map.heads)
  File "/home/alex/.pyenv/versions/3.8.6/envs/minimal-app/lib/python3.8/site-packages/alembic/util/langhelpers.py", line 234, in __get__
    obj.__dict__[self.__name__] = result = self.fget(obj)
  File "/home/alex/.pyenv/versions/3.8.6/envs/minimal-app/lib/python3.8/site-packages/alembic/script/revision.py", line 104, in heads
    self._revision_map
  File "/home/alex/.pyenv/versions/3.8.6/envs/minimal-app/lib/python3.8/site-packages/alembic/util/langhelpers.py", line 234, in __get__
    obj.__dict__[self.__name__] = result = self.fget(obj)
  File "/home/alex/.pyenv/versions/3.8.6/envs/minimal-app/lib/python3.8/site-packages/alembic/script/revision.py", line 155, in _revision_map
    for revision in self._generator():
  File "/home/alex/.pyenv/versions/3.8.6/envs/minimal-app/lib/python3.8/site-packages/alembic/script/base.py", line 112, in _load_revisions
    script = Script._from_filename(self, vers, file_)
  File "/home/alex/.pyenv/versions/3.8.6/envs/minimal-app/lib/python3.8/site-packages/alembic/script/base.py", line 913, in _from_filename
    return Script(module, revision, os.path.join(dir_, filename))
  File "/home/alex/.pyenv/versions/3.8.6/envs/minimal-app/lib/python3.8/site-packages/alembic/script/base.py", line 696, in __init__
    super(Script, self).__init__(
  File "/home/alex/.pyenv/versions/3.8.6/envs/minimal-app/lib/python3.8/site-packages/alembic/script/revision.py", line 1041, in __init__
    raise LoopDetected(revision)
alembic.script.revision.LoopDetected: Self-loop is detected in revisions (user)

With alembic 1.4.3:

$ flask db heads
user (head)

Versions.

  • OS: Ubuntu 20.04
  • Python: 3.8.6
  • Alembic: 1.5.1
  • SQLAlchemy: 1.3.22
  • Database: Postgres 12
  • DBAPI: psycopg2 2.8.6

Additional context

I suspect that this problem has arisen with the change in #758. From reading the code, I think there's an error on line 1037(in the PR) of revision.py. Where it's doing if down_revision and revision in down_revision: I think it should probably be doing if down_revision and revision == down_revision:.

Have a nice day!

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions