1

In fact surely it does, but I'm certainly doing something wrong.

I've got a database where I create a FK on a table and in a future migration, it's dropped.

So the create statement of the table is:

CREATE TABLE this_table (
    pk INTEGER NOT NULL,
    some_other_table_pk INTEGER,
    CONSTRAINT "fk_436d3a57-0ae2-5668-937d-8cbc82d15172" FOREIGN KEY(some_other_table_pk) REFERENCES some_other_table (pk)

Then I drop the constraint and alembic auto generates the following batch operation:

def upgrade():
    with op.batch_alter_table('this_table', schema=None) as batch_op:
        # how does it know here which fk to drop? there are more than one???
        batch_op.drop_constraint(None, type_='foreignkey')
        batch_op.drop_column('some_other_table_pk')

def downgrade():
    with op.batch_alter_table('this_table', schema=None) as batch_op:
        batch_op.add_column(sa.Column('some_other_table_pk', sa.INTEGER(), nullable=True))
        batch_op.create_foreign_key(None, 'some_other_table_pk', ['some_other_table'], ['pk'])

Which leads to an error on upgrade head:

KeyError: 'fk_ad156c6d-7a24-5d8e-868e-2fb00c2cb1c9'

so clearly it didn't regenerate the same key?!? The key doesn't match any fk on this table... But how should it know which key to generate? drop_constraint didn't get any info about which fk to drop?

my MetaDataConventions:

def fk_guid(constraint, table):
    str_tokens = [
        table.name,
    ] + [
        element.parent.name for element in constraint.elements
    ] + [
        element.target_fullname for element in constraint.elements
    ]
    guid = uuid.uuid5(uuid.NAMESPACE_OID, "_".join(str_tokens))
    return str(guid)

convention = {
  "fk_guid": fk_guid,
  "ix": "ix_%(column_0_label)s",
  "uq": "uq_%(table_name)s_%(column_0_name)s",
  "ck": "ck_%(table_name)s_%(column_0_name)s",
  "fk": "fk_%(fk_guid)s",
  "pk": "pk_%(table_name)s",
}

metadata = MetaData(naming_convention=convention)

Base = declarative_base(metadata=metadata)

and in env.py:

# ...
target_metadata = models.Base.metadata
# ...
def run_migrations_offline():
    url = getUrl()
    context.configure(
        url=url,
        target_metadata=target_metadata,
        literal_binds=True,
        render_as_batch=True,
        dialect_opts={"paramstyle": "named"},
        compare_type=True
    )

    with context.begin_transaction():
        context.run_migrations()

def run_migrations_online():
    alembic_config = config.get_section(config.config_ini_section)

    connectable = create_engine(getUrl())

    with connectable.connect() as connection:
        context.configure(
            connection=connection,
            target_metadata=target_metadata,
            render_as_batch=True,
            dialect_opts={"paramstyle": "named"},
            compare_type=True
        )

        with context.begin_transaction():
            context.run_migrations()

Database is a sqlite3 file.

start of edit

So I tried to manually pass in the name of the constraint to drop:

def upgrade():
    with op.batch_alter_table('this_table', schema=None) as batch_op:
        batch_op.drop_constraint('fk_436d3a57-0ae2-5668-937d-8cbc82d15172', type_='foreignkey')
        batch_op.drop_column('some_other_table_pk')

And now it resulted in a key error but the key should have been present in the DB?!?

KeyError: 'fk_436d3a57-0ae2-5668-937d-8cbc82d15172'

end of edit

start of edit2

OK so I played a bit in the sandbox, and it seems that it's user defined token keys which are not working on upgrade if it's a drop_constraint:

fk_guid in the case below is only called if the constraint is created

convention = {
  "fk_guid": some_function,
  "ix": "ix_%(column_0_label)s",
  "uq": "uq_%(table_name)s_%(column_0_name)s",
  "ck": "ck_%(table_name)s_%(column_0_name)s",
  "fk": "fk_%(fk_guid)s",
#  "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
  "pk": "pk_%(table_name)s",
}

And gives the following migration code for creation and dropping:

sa.ForeignKeyConstraint(['table_b'], ['table_b.id'], name=op.f('fk_9d68bd30-b17a-565a-aee7-7bd21e90672e')),

# But for dropping, it's not generated:
batch_op.drop_constraint(None, type_='foreignkey')

But strangely enough, some_function is getting called on alembic upgrade head but not with the same constraints as on the creation call:

def fk_guid(constraint, table):
    str_tokens = [
        table.name,
    ] + [
        element.parent.name for element in constraint.elements
    ] + [
        element.target_fullname for element in constraint.elements
    ]

    print(f'string_tokens: {str_tokens}')

    guid = uuid.uuid5(uuid.NAMESPACE_OID, "_".join(str_tokens))
    return str(guid)

def some_function(*params):
    print(f'params: {params}')
    return fk_guid(*params)

Output:

string_tokens: ['table_a']

Changing the naming convention of foreign key to standard

convention = {
  "fk_guid": some_function,
  "ix": "ix_%(column_0_label)s",
  "uq": "uq_%(table_name)s_%(column_0_name)s",
  "ck": "ck_%(table_name)s_%(column_0_name)s",
#  "fk": "fk_%(fk_guid)s",
  "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
  "pk": "pk_%(table_name)s",
}

Gives creation and dropping as wanted:

# creation:
sa.ForeignKeyConstraint(['table_b'], ['table_b.id'], name=op.f('fk_table_a_table_b_table_b')),

# dropping:
batch_op.drop_constraint('fk_table_a_table_b_table_b', type_='foreignkey')

end of edit2

start of edit3

There's a thread on github: https://github.com/sqlalchemy/alembic/issues/713

end of edit3

PirklW
  • 484
  • 2
  • 16
  • I didn't find the answer to this yet... anybody? – PirklW Jul 15 '20 at 06:12
  • I'd recommend you to name your constraints in order to debug this. What you have seems autogenerated and it's quite difficult to understand from here. ` – Cheche Jul 16 '20 at 23:46
  • the constraint names are auto generated using UUIDs, or do I misunderstand something fundamental? Even if I tell alembic to drop the exact name of the constraint, it doesn't seem to find it. – PirklW Jul 17 '20 at 08:59
  • Great you solved it!, Why don't you split this in question and answer so anyone can benefit from your finding. – Cheche Jul 18 '20 at 18:57
  • @cheche, well I wouldn't say solution, it's a work around and it still doesn't work with sqlite3 – PirklW Jul 20 '20 at 06:11

0 Answers0