alembic icon indicating copy to clipboard operation
alembic copied to clipboard

Autogenerate doesn't correctly handle postgresql enums

Open sqlalchemy-bot opened this issue 10 years ago • 37 comments

Migrated issue, originally created by Ulrich Petri (@ulope)

According to #67 enum types used in columns have to be explicitly created and destroyed.

However the autogenerator currently doesn't produce those explicit commands. This problem is made worse by the fact that the autogenerated migration appears to work fine the first time it is applied and even removed. The dangling enum type only causes a conflict the second time the migration is applied.

edit by mike: note also that anything we do for PG native enums also has to run outside of a transaction block, so this is really not going to be easy. #123 provides the feature for this however if autogenerate, or the operation itself, is going to make that happen automatically, that's a very dramatic behavior.

sqlalchemy-bot avatar Feb 25 '15 17:02 sqlalchemy-bot

Michael Bayer (@zzzeek) wrote:

#67 is probably not a great built-in solution to this issue, as that solution does not accommodate for the fact that ENUM has different implementations on different backends.

The issue here might be mostly the same as #122. The issue of PG ENUM is mostly unhandled as there are many issues and use cases to be worked out. The emission of the CREATE/DROP might be best automated as within the handling of Enum itself, but then this is tricky as in #122 the type might already exist or not.

Basically I don't have a solution to this right now which covers every case and that is the majority of work to be done.

sqlalchemy-bot avatar Feb 25 '15 22:02 sqlalchemy-bot

Leonardo (@leotada) wrote:

Why does not check if the type exists and tries to create if not, or alter if the name is the same. https://stackoverflow.com/questions/7624919/check-if-a-user-defined-type-already-exists-in-postgresql

sqlalchemy-bot avatar Jul 27 '17 13:07 sqlalchemy-bot

Michael Bayer (@zzzeek) wrote:

@leotada sure. The work is, "works with the generic Enum as well as PG ENUM, doesn't interfere with other backends, has plenty of functional tests".

sqlalchemy-bot avatar Jul 27 '17 14:07 sqlalchemy-bot

Michael Bayer (@zzzeek) wrote:

Issue #443 was marked as a duplicate of this issue.

sqlalchemy-bot avatar Aug 25 '17 13:08 sqlalchemy-bot

Michael Bayer (@zzzeek) wrote:

Issue #122 was marked as a duplicate of this issue.

sqlalchemy-bot avatar Sep 18 '17 06:09 sqlalchemy-bot

Michael Bayer (@zzzeek) wrote:

Issue #471 was marked as a duplicate of this issue.

sqlalchemy-bot avatar Jan 01 '18 20:01 sqlalchemy-bot

Michael Bayer (@zzzeek) wrote:

Issue #484 was marked as a duplicate of this issue.

sqlalchemy-bot avatar Mar 02 '18 18:03 sqlalchemy-bot

Michael Bayer (@zzzeek) wrote:

Issue #488 was marked as a duplicate of this issue.

sqlalchemy-bot avatar Mar 22 '18 13:03 sqlalchemy-bot

Changes by Michael Bayer (@zzzeek):

  • removed labels: autogenerate - rendering
  • added labels: postgresql typing issues

sqlalchemy-bot avatar Feb 25 '15 22:02 sqlalchemy-bot

Changes by Michael Bayer (@zzzeek):

  • changed title from "Autogenerate doesn't correctly handle enums" to "Autogenerate doesn't correctly handle postgresql e"

sqlalchemy-bot avatar Feb 25 '15 22:02 sqlalchemy-bot

I'm quite confused right now.

Issue #471 was closed because it was deemed a duplicate of this issue (which is specific to postgresql ENUMs).

But downgrade scripts aren't being generated for sqlalchemy.Enums either.

matthhong avatar Feb 05 '20 03:02 matthhong

But downgrade scripts aren't being generated for sqlalchemy.Enums either.

A generic Enum is a VARCHAR in the database. If you add or remove items to the enum, there is no datatype to be changed. There is a CHECK constraint, unfortunately, which Alembic does not have support for right now, support for this would be #363.

zzzeek avatar Feb 05 '20 14:02 zzzeek

Lot of dupes but hard to find information for how to do this yourself. Here's an example of a migration to add a postgres enum to a pre-existing table.

def upgrade():
    status_enum = postgresql.ENUM('pending', 'accepted', 'canceled', name='mytable_status_enum')
    status_enum.create(op.get_bind())
    op.add_column('mytable', sa.Column('status', status_enum))

def downgrade():
    op.drop_column('mytable', 'status')
    op.execute("DROP TYPE mytable_status_enum;")

AFlowOfCode avatar Aug 03 '20 20:08 AFlowOfCode

Workaround to reuse enum in backend-agnostic (I hope) case:

def upgrade():
    connection = op.get_bind()
    if connection.dialect.name == "postgresql":
        status_enum = postgresql.ENUM('pending', 'accepted', 'canceled', name='mytable_status_enum', create_type=False)
    else:
        status_enum = sa.Enum('pending', 'accepted', 'canceled', name='mytable_status_enum')
    op.add_column('mytable', sa.Column('status', status_enum))

ziima avatar Aug 11 '20 11:08 ziima

Workaround based on @zzzeek's answer from sqlalchemy/sqlalchemy#5593:

def upgrade():
    status_enum_postgres = postgresql.ENUM('pending', 'accepted', 'canceled', name='mytable_status_enum', create_type=False)
    status_enum = sa.Enum('pending', 'accepted', 'canceled', name='mytable_status_enum')
    status_enum.with_variant(status_enum_postgres, 'postgresql')
    op.add_column('mytable', sa.Column('status', status_enum))

It seems to be working fine.

ziima avatar Dec 22 '20 14:12 ziima

Workaround based on @zzzeek's answer from sqlalchemy/sqlalchemy#5593:

def upgrade():
    status_enum_postgres = postgresql.ENUM('pending', 'accepted', 'canceled', name='mytable_status_enum', create_type=False)
    status_enum = sa.Enum('pending', 'accepted', 'canceled', name='mytable_status_enum')
    status_enum.with_variant(status_enum_postgres, 'postgresql')
    op.add_column('mytable', sa.Column('status', status_enum))

It seems to be working fine.

This solution seems to not work for that latest version 1.5.8. We tried this codes:

status_enum_postgres = postgresql.ENUM('pending', 'accepted', 'canceled', name='emotion', create_type=False)
status_enum_postgres.create(op.get_bind(), checkfirst=False)
op.add_column('mytable', sa.Column('status', status_enum_postgres, nullable=True))

It works great.

JohnnyFee avatar Apr 14 '21 03:04 JohnnyFee

@JohnnyFee My solution works for me at alembic 1.5.8, but I still use sqlalchemy 1.3, which may be the difference. I still encounter type "mytable_status_enum" already exists error, when I try your solution.

ziima avatar Apr 26 '21 16:04 ziima

Is there a way to just reuse the enum? or should I create another different one for each table?

like I have this enum: enabled, disabled, pending

In lots of different places, the best way would be to create a different enum for each one?

tyoc213 avatar May 11 '21 17:05 tyoc213

Is there a way to just reuse the enum? or should I create another different one for each table?

like I have this enum: enabled, disabled, pending

In lots of different places, the best way would be to create a different enum for each one?

you can make just one Enum object and reuse it, sure. In PostgreSQL, the enum is a totally separate construct independent of the table.

zzzeek avatar May 12 '21 16:05 zzzeek

Workaround based on @zzzeek's answer from sqlalchemy/sqlalchemy#5593:

def upgrade():
    status_enum_postgres = postgresql.ENUM('pending', 'accepted', 'canceled', name='mytable_status_enum', create_type=False)
    status_enum = sa.Enum('pending', 'accepted', 'canceled', name='mytable_status_enum')
    status_enum.with_variant(status_enum_postgres, 'postgresql')
    op.add_column('mytable', sa.Column('status', status_enum))

It seems to be working fine.

This solution seems to not work for that latest version 1.5.8.

@JohnnyFee (and for posterity) - the snippet which @ziima posted appears to not work for me also (sqla 1.4.22, alembic 1.6.5), but it's because with_variant() actually returns the variant type, rather than modifying the type in place. I didn't follow your approach because I didn't want to do postgres stuff directly in the migration.

My specific use case is to use an existing enum for a column in a new table which is being constructed from an auto-generated migration. I've found that I can use the with_variant() trick like so (with imports included to make it more clear to other new folks where ENUM() comes from):

import sqlalchemy as sa
import sqlalchemy.dialects.postgresql as sad_psql

def upgrade():
    vals, name = ("foo", "bar", "baz"), "my_enum"
    sa_enum = sa.Enum(*vals, name=name)
    psql_enum = sad_psql.ENUM(*vals, name=name, create_type=False)
    variant_enum = sa_enum.with_variant(psql_enum, "postgresql")
    # Now use `variant_enum` anywhere the autogenerated `sa.Enum()` is used below
    # e.g.
    op.create_table("my_table",
        sa.Column("val", variant_enum),
    )

This works for my case where the revision history ensures that the enum is already present in the database. I'm not confident that there's an easy way to create the enum only if necessary while creating a table since passing checkfirst=True down to the enum looks complicated. The variant approach is quite nice since it allows us to avoid doing anything postgres-y unless the migration is actually running against a backend using that dialect, but the logic is well-defined for that case.


I've also toyed with the idea of creating the table and then immediately doing an op.add_column() without specifically calling enum_type.create(op.get_bind()), since I know it's already defined and the column can be added without error. I prefer including it in the table creation using the variant trick, but if I were to have to add a column to an existing table it could look like:

def upgrade():
    vals, name = ("foo", "bar", "baz"), "my_enum"
    # This works if the enum is already defined since adding a column doesn't attempt to create the enum
    op.add_column("my_table", sa.Column("val", sa.Enum(*vals, name=name)))

or:

def upgrade():
    vals, name = ("foo", "bar", "baz"), "my_enum"
    sa_enum = sa.Enum(*vals, name=name)
    # This will create the enum if it's not already present which is helpful
    sa_enum.create(op.get_bind(), checkfirst=True)
    op.add_column("my_table", sa.Column(sa_enum))

or even:

def upgrade():
    vals, name = ("foo", "bar", "baz"), "my_enum"
    sa_enum = sa.Enum(*vals, name=name)
    psql_enum = sad_psql.ENUM(*vals, name=name, create_type=False)
    variant_enum = sa_enum.with_variant(psql_enum, "postgresql")
    # This doesn't seem to provide much value beyond an `sa.Enum().create(.., checkfirst=True)
    # but maybe there's an edge case where it'd be useful - or it could be preferable to always use
    # the variant type to stay DRY?
    op.add_column("my_table", sa.Column(variant_enum))

maybe-sybr avatar Aug 24 '21 05:08 maybe-sybr

I'm using the second version proposed by @maybe-sybr and it works like a charm. For downgrading I use:

def downgrade():
    sa_enum = sa.Enum(name='my_enum')
    sa_enum.drop(op.get_bind(), checkfirst=True)

bechtold avatar Aug 27 '21 15:08 bechtold

def upgrade():
    vals, name = ("foo", "bar", "baz"), "my_enum"
    sa_enum = sa.Enum(*vals, name=name)
    psql_enum = sad_psql.ENUM(*vals, name=name, create_type=False)
    variant_enum = sa_enum.with_variant(psql_enum, "postgresql")
    # This doesn't seem to provide much value beyond an `sa.Enum().create(.., checkfirst=True)
    # but maybe there's an edge case where it'd be useful - or it could be preferable to always use
    # the variant type to stay DRY?
    op.add_column("my_table", sa.Column(variant_enum))
def upgrade():
    vals, name = ("foo", "bar", "baz"), "my_enum"
    sa_enum = sa.Enum(*vals, name=name)
    psql_enum = sad_psql.ENUM(*vals, name=name, create_type=False)
    variant_enum = sa_enum.with_variant(psql_enum, "postgresql")
    # This doesn't seem to provide much value beyond an `sa.Enum().create(.., checkfirst=True)
    # but maybe there's an edge case where it'd be useful - or it could be preferable to always use
    # the variant type to stay DRY?
    op.add_column("my_table", sa.Column(variant_enum))

Doesn't work with 1.80 on Ubuntu/Python 3.10.4

and

def upgrade():
    vals, name = ("foo", "bar", "baz"), "my_enum"
    sa_enum = sa.Enum(*vals, name=name)
    # This will create the enum if it's not already present which is helpful
    sa_enum.create(op.get_bind(), checkfirst=True)
    op.add_column("my_table", sa.Column(sa_enum))

works for me

gainskills avatar Jun 09 '22 01:06 gainskills


#from ... import StatusEnum
class StatusEnum(enum.Enum):
    none ="none"
    ready="ready"
    runing="running"
    fail="fail"
    success="success"
class Author(db.Model):
    __tablename__ = "authors"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String, nullable=False)
    status = db.Column( db.Enum(StatusEnum),default=StatusEnum.none)
    def __repr__(self):
        return "<Author(name={self.name!r})>".format(self=self)
def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    status = sa.Enum(StatusEnum, name="statusenum")
    status.create(op.get_bind(), checkfirst=True)
    op.add_column('authors', sa.Column('status', sa.Enum('none', 'ready', 'running', 'fail', 'success', name='statusenum'), server_default='none', nullable=False))
    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_column('authors', 'status')
    status = sa.Enum(StatusEnum, name="statusenum")
    status.drop(op.get_bind())
    # ### end Alembic commands ###

works for me

wllzhang avatar Sep 17 '22 19:09 wllzhang

This 3rd party hook worked for me, to autogenerate migrations to changes in enum values with pg, https://github.com/dw/alembic-autogenerate-enums

antont avatar Sep 28 '22 10:09 antont

I have had success modifying the render_item function in the env.py file as shown below, am I introducing any obvious errors?:

# normal env.py content
from sqlalchemy import Enum
def render_item(type_, obj, autogen_context):
    """Apply custom rendering for Postgres enum,
    this is a "temporary" fix until this alembic issue is resolved: https://github.com/sqlalchemy/alembic/issues/278.
    """
    if type_ == "type" and isinstance(obj, Enum):
        enums = ", ".join([f"'{enum_val}'" for enum_val in obj.enums])
        return f'sa.Enum({enums}, name="{obj.name}", create_type=False)'

    # default rendering for other objects
    return False
    
    
def run_migrations_online() -> None:
    # ...
    with connectable.connect() as connection:
        context.configure(
            connection=connection,
            target_metadata=target_metadata,
            render_item=render_item,
        )

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

A sample use is:

class User(Base):
    __tablename__ = "users"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, unique=True)
    role = Column(RoleEnumType)
    roles = Column(ARRAY(RoleEnumType))

# this part is shown here: https://www.horsfallnathan.com/blog/sqlalchemy-enums-in-python
class RoleEnum(str, Enum):
    ADMIN = "admin"
    GUEST = "guest"

RoleEnumType = Enum(
    RoleEnum,
    name="role_enum_type",
    values_callable=lambda obj: [item.value for item in obj],
)

horsfallnathan avatar Mar 10 '23 09:03 horsfallnathan

The autogenerate works for me if I bind it to the engine and do something like Model.__table__.create(bind=raw_engine). Using CreateTable leads to strange behavior like "enum already exists" or if it doesn't exist, complains that it can't create it.

deeTEEcee avatar Jun 06 '23 20:06 deeTEEcee

import sqlalchemy as sa

existing_type = sa.Enum(
    "A",
    "B",
    name="{type_name}",
)

new_type = sa.Enum(
    "A",
    "B",
    "C",
    name="{type_name}",
)

def upgrade():
    op.alter_column(
        "{table_name}",
        "{column_name}",
        existing_type=existing_type,
        type_=new_type,
        existing_server_default="{default_value}", # needed for mysql
        existing_nullable=False, # needed for mysql
    )

def downgrade():
    op.alter_column(
        "{table_name}",
        "{column_name}",
        existing_type=new_type,
        type_=existing_type,
        existing_server_default="{default_value}",
        existing_nullable=False,
    )

Although this issue is for postgresql, fyi, I was able to do this with mysql

taemin410 avatar Jun 23 '23 12:06 taemin410

I don't think it will actually add a new enum value using the above example on postgressql. It should render an ALTER TYPE {type_name} ADD VALUE 'C' AFTER 'B', but there is currently nothing that does this in alembic, so you will need to run this command manually

CaselIT avatar Jun 23 '23 14:06 CaselIT

I have created a library that can help with this issue.

It will track the occurrence, deletion and modification of enums and add the appropriate commands to the migration

Just install library

pip install alembic-postgresql-enum

and add the line in migrations/env.py

import alembic_postgresql_enum

You won't have to manually write raw sql enum migrations anymore

RustyGuard avatar Aug 09 '23 16:08 RustyGuard

@RustyGuard nice work. Quickly glancing at the lib I think some of the simple parts could be integrated in alembic, but I would not be very comfortable with the enum alter operations.

In any case, maybe it makes sense mentioning this library somewhere in the docs. @zzzeek is the best place the cookbook?

CaselIT avatar Aug 09 '23 16:08 CaselIT