지수환
지수환

Reputation: 53

Add / remove a value of PostgreSQL ENUM type in alembic

I have a trouble with altering existing postgresql.ENUM column with SQLAlchemy and Alembic.

I want to add / remove a value to postgresql.ENUM type column in alembic.

In specific, current enum type is created by the following two alembic revisions:

# revision 1
def upgrade():
    op.create_table('kernels',
        sa.Column('status', sa.String(), nullable=True),
        ...
    )


# revision 2
kernelstatus_choices = (
    'PREPARING', 'BUILDING', 'RUNNING',
    'RESTARTING', 'RESIZING', 'SUSPENDED',
    'TERMINATING', 'TERMINATED', 'ERROR',
)
kernelstatus = postgresql.ENUM(
    *kernelstatus_choices,
    name='kernelstatus')

def upgrade():
    op.alter_column('kernels', column_name='status', 
                    type_=sa.Enum(*kernelstatus_choices, name='kernelstatus'),
                    postgresql_using='status::kernelstatus')

Now, I want to add 'PENDING' status to kernelstatus type. So I implemented like below, by referencing some articles.

prev_kernelstatus_choices = (
    'PREPARING', 'BUILDING', 'RUNNING',
    'RESTARTING', 'RESIZING', 'SUSPENDED',
    'TERMINATING', 'TERMINATED', 'ERROR',
)

prev_kernelstatus = postgresql.ENUM(
    *prev_kernelstatus_choices,
    name='kernelstatus')

curr_kernelstatus_choices = ('PENDING',) + prev_kernelstatus_choices

curr_kernelstatus = postgresql.ENUM(
    *curr_kernelstatus_choices,
    name='kernelstatus')

def upgrade():
    op.execute('ALTER TYPE kernelstatus RENAME TO kernelstatus_old;')
    curr_kernelstatus.create(op.get_bind())
    op.alter_column('kernels', column_name='status', 
                    type_=sa.Enum(*curr_kernelstatus_choices, name='kernelstatus'),
                    postgresql_using='status::text::kernelstatus')
    op.execute('DROP TYPE kernelstatus_old;')

But it keeps generating the following error:

sqlalchemy.exc.ProgrammingError: (psycopg2.ProgrammingError) operator does not exist: kernelstatus <> kernelstatus_old
HINT:  No operator matches the given name and argument type(s). You might need to add explicit type casts.
[SQL: 'ALTER TABLE kernels ALTER COLUMN status TYPE kernelstatus USING status::text::kernelstatus']

I already tried a solution with adding value to enum type, but this does not work in with Alembic since each Alembic revision runs in a transaction and ALTER TYPE statement cannot run in a transaction. Also, there should be a code for downgrade() and there is no statement for removing a value from enum type in PostgreSQL, so just adding a value to the enum type cannot be the ultimate solution in my case.

Could somebody give me a help?

Upvotes: 5

Views: 7564

Answers (1)

Achimnol
Achimnol

Reputation: 1599

I figured out the root cause of this case (I've done collaboration with the questioner): since the kernels table has a unique constraint that checks the uniqueness of (the sess_id column, the status column of the enum type kernelstatus when its value is 'TERMINATED').

Converting the enum type of the status column requires an explicit definition of <> SQL operator to keep this constraint check consistent across the conversion. The problem was that PostgreSQL's error message just said "operator does not exist" rather than "operator must be defined to keep constraint during conversion".

So, I could write a working migration, by temporarily adding a comparison operator implemented by converting both old/new enum values into strings:

from alembic import op
import sqlalchemy as sa
import textwrap
from sqlalchemy.dialects import postgresql

kernelstatus_new_values = [
    'PENDING',     # added
    'PREPARING',
    ...
]
kernelstatus_new = postgresql.ENUM(*kernelstatus_new_values, name='kernelstatus')

kernelstatus_old_values = [
    'PREPARING',
    ...
]
kernelstatus_old = postgresql.ENUM(*kernelstatus_old_values, name='kernelstatus')

def upgrade():
    conn = op.get_bind()
    sessionresult.create(conn)
    sessiontypes.create(conn)
    conn.execute('ALTER TYPE kernelstatus RENAME TO kernelstatus_old;')
    kernelstatus_new.create(conn)
    conn.execute(textwrap.dedent('''\
    CREATE FUNCTION new_old_compare(
        new_enum_val kernelstatus, old_enum_val kernelstatus_old
    )
        RETURNS boolean AS $$
            SELECT new_enum_val::text <> old_enum_val::text;
        $$ LANGUAGE SQL IMMUTABLE;

    CREATE OPERATOR <> (
        leftarg = kernelstatus,
        rightarg = kernelstatus_old,
        procedure = new_old_compare
    );
    '''))
    op.alter_column(
        table_name='kernels',
        column_name='status',
        type_=kernelstatus_new,
        postgresql_using='status::text::kernelstatus',
    )
    conn.execute(textwrap.dedent('''\
    DROP FUNCTION new_old_compare(
        new_enum_val kernelstatus, old_enum_val kernelstatus_old
    ) CASCADE;
    DROP TYPE kernelstatus_old;
    '''))

    ...  # the rest of migration

Upvotes: 2

Related Questions