Подтвердить что ты не робот

Изменение поля Enum с использованием Alembic

Как добавить элемент в поле Enum в миграции alembic при использовании версии PostgreSQL старше 9.1 (которая добавляет ALTER TYPE для перечислений)? Это. Вопрос SO объясняет прямой процесс, но я не совсем уверен, как лучше всего перевести его с помощью alembic.

Это то, что у меня есть:

new_type = sa.Enum('nonexistent_executable', 'output_limit_exceeded',
                   'signal', 'success', 'timed_out', name='status')
old_type = sa.Enum('nonexistent_executable', 'signal', 'success', 'timed_out',
                   name='status')
tcr = sa.sql.table('testcaseresult',
                   sa.Column('status', new_type, nullable=False))


def upgrade():
    op.alter_column('testcaseresult', u'status', type_=new_type,
                    existing_type=old_type)


def downgrade():
    op.execute(tcr.update().where(tcr.c.status==u'output_limit_exceeded')
               .values(status='timed_out'))
    op.alter_column('testcaseresult', u'status', type_=old_type,
                    existing_type=new_type)

Вышеприведенное, к сожалению, производит только ALTER TABLE testcaseresult ALTER COLUMN status TYPE status при обновлении, что практически ничего не делает.

4b9b3361

Ответ 1

Я решил попробовать как можно быстрее следовать postgres и придумал следующую миграцию.

from alembic import op
import sqlalchemy as sa

old_options = ('nonexistent_executable', 'signal', 'success', 'timed_out')
new_options = sorted(old_options + ('output_limit_exceeded',))

old_type = sa.Enum(*old_options, name='status')
new_type = sa.Enum(*new_options, name='status')
tmp_type = sa.Enum(*new_options, name='_status')

tcr = sa.sql.table('testcaseresult',
                   sa.Column('status', new_type, nullable=False))


def upgrade():
    # Create a tempoary "_status" type, convert and drop the "old" type
    tmp_type.create(op.get_bind(), checkfirst=False)
    op.execute('ALTER TABLE testcaseresult ALTER COLUMN status TYPE _status'
               ' USING status::text::_status')
    old_type.drop(op.get_bind(), checkfirst=False)
    # Create and convert to the "new" status type
    new_type.create(op.get_bind(), checkfirst=False)
    op.execute('ALTER TABLE testcaseresult ALTER COLUMN status TYPE status'
               ' USING status::text::status')
    tmp_type.drop(op.get_bind(), checkfirst=False)


def downgrade():
    # Convert 'output_limit_exceeded' status into 'timed_out'
    op.execute(tcr.update().where(tcr.c.status==u'output_limit_exceeded')
               .values(status='timed_out'))
    # Create a tempoary "_status" type, convert and drop the "new" type
    tmp_type.create(op.get_bind(), checkfirst=False)
    op.execute('ALTER TABLE testcaseresult ALTER COLUMN status TYPE _status'
               ' USING status::text::_status')
    new_type.drop(op.get_bind(), checkfirst=False)
    # Create and convert to the "old" status type
    old_type.create(op.get_bind(), checkfirst=False)
    op.execute('ALTER TABLE testcaseresult ALTER COLUMN status TYPE status'
               ' USING status::text::status')
    tmp_type.drop(op.get_bind(), checkfirst=False)

Похоже, что alembic не имеет прямой поддержки оператора USING в своем методе alter_table.

Ответ 2

Я использовал более простой подход с меньшими шагами, чем принятый ответ, на котором я основывал это. В этом примере я буду притворяться, что указанное перечисление называется "status_enum", потому что в принятом ответе использование "status" для столбца и enum меня путало.

from alembic import op 
import sqlalchemy as sa

name = 'status_enum'
tmp_name = 'tmp_' + name

old_options = ('nonexistent_executable', 'signal', 'success', 'timed_out')
new_options = sorted(old_options + ('output_limit_exceeded',))

new_type = sa.Enum(*new_options, name=name)
old_type = sa.Enum(*old_options, name=name)

tcr = sa.sql.table('testcaseresult',
                   sa.Column('status', new_type, nullable=False))

def upgrade():
    op.execute('ALTER TYPE ' + name + ' RENAME TO ' + tmp_name)

    new_type.create(op.get_bind())
    op.execute('ALTER TABLE testcaseresult ALTER COLUMN status ' +
               'TYPE ' + name + ' USING status::text::' + name)
    op.execute('DROP TYPE ' + tmp_name)


def downgrade():
    # Convert 'output_limit_exceeded' status into 'timed_out'                                                                                                                      
    op.execute(tcr.update().where(tcr.c.status=='output_limit_exceeded')
               .values(status='timed_out'))

    op.execute('ALTER TYPE ' + name + ' RENAME TO ' + tmp_name)

    old_type.create(op.get_bind())
    op.execute('ALTER TABLE testcaseresult ALTER COLUMN status ' +
               'TYPE ' + name + ' USING status::text::' + name)
    op.execute('DROP TYPE ' + tmp_name)

Ответ 3

По состоянию на Postgres 9.1 добавление нового значения в перечисление можно выполнить с помощью оператора ALTER TYPE. Это осложняется тем, что это невозможно сделать в транзакции. Однако это можно обойти, совершив транзакцию alembic см. Здесь.

У меня были проблемы с использованием более старого, более подробного решения, потому что Postgres не смог автоматически преобразовать значение по умолчанию для столбца.

Ответ 4

В прямом SQL это будет работать для Postgres, если порядок вещей в вашем перечислении не должен быть таким, как указано выше:

ALTER TYPE status ADD value 'output_limit_exceeded' after 'timed_out'; 

Ответ 5

У меня была та же проблема, что и при переносе типа столбца в другой. Я использую следующие требования:

Alembic==0.9.4
SQLAlchemy==1.1.12 

Вы можете указать аргумент postgresql_using как kwarg alembic.op.alter_column.

from alembic import op
import sqlalchemy as types

op.alter_column(
    table_name='my_table',
    column_name='my_column',
    type_=types.NewType,
    # allows to use postgresql USING
    postgresql_using="my_column::PostgesEquivalentOfNewType",
)

Надеюсь, это поможет.