Adicionando um novo valor a um tipo ENUM existente

208

Eu tenho uma coluna da tabela que usa um enumtipo. Desejo atualizar esse enumtipo para ter um valor adicional possível. Não quero excluir nenhum valor existente, basta adicionar o novo valor. Qual é a maneira mais simples de fazer isso?

Ian
fonte

Respostas:

153

OBSERVAÇÃO Se você estiver usando o PostgreSQL 9.1 ou posterior, e está autorizado a fazer alterações fora de uma transação, consulte esta resposta para uma abordagem mais simples.


Eu tive o mesmo problema há alguns dias e encontrei este post. Portanto, minha resposta pode ser útil para alguém que está procurando solução :)

Se você tiver apenas uma ou duas colunas que usam o tipo de enumeração que deseja alterar, tente isso. Além disso, você pode alterar a ordem dos valores no novo tipo.

-- 1. rename the enum type you want to change
alter type some_enum_type rename to _some_enum_type;
-- 2. create new type
create type some_enum_type as enum ('old', 'values', 'and', 'new', 'ones');
-- 3. rename column(s) which uses our enum type
alter table some_table rename column some_column to _some_column;
-- 4. add new column of new type
alter table some_table add some_column some_enum_type not null default 'new';
-- 5. copy values to the new column
update some_table set some_column = _some_column::text::some_enum_type;
-- 6. remove old column and type
alter table some_table drop column _some_column;
drop type _some_enum_type;

3-6 deve ser repetido se houver mais de uma coluna.

taksofan
fonte
9
Vale ressaltar que tudo isso pode ser feito em uma única transação, portanto é mais seguro fazê-lo em um banco de dados de produção.
David Leppik 08/07
52
Isso nunca foi uma boa ideia. Desde a versão 9.1, você pode fazer tudo isso ALTER TYPE. Mas mesmo antes disso, ALTER TABLE foo ALTER COLUMN bar TYPE new_type USING bar::text::new_type;era muito superior.
precisa
1
Esteja ciente de que as versões mais antigas do Postgres não suportam tipos de renomeação. Especificamente, a versão do Postgres no Heroku (banco de dados compartilhado, acredito que eles usam o PG 8.3) não a suporta.
Ortwin Gentz
13
Você pode recolher os passos 3, 4, 5 e 6 juntos em uma única instrução:ALTER TABLE some_table ALTER COLUMN some_column TYPE some_enum_type USING some_column::text::some_enum_type;
glyphobet
3
Se fizer isso em uma mesa ativa, bloqueie a mesa durante o procedimento. O nível de isolamento da transação padrão no postgresql não impedirá que novas linhas sejam inseridas por outras transações durante essa transação; portanto, você pode ficar com linhas preenchidas incorretamente.
Sérgio Carvalho
422

O PostgreSQL 9.1 introduz a capacidade dos tipos ALTER Enum:

ALTER TYPE enum_type ADD VALUE 'new_value'; -- appends to list
ALTER TYPE enum_type ADD VALUE 'new_value' BEFORE 'old_value';
ALTER TYPE enum_type ADD VALUE 'new_value' AFTER 'old_value';
Dariusz
fonte
1
qual é o "enum_type"? nome do campo, nome do campo_tabela? ou alguma outra coisa? como devo acertar isso? Eu tenho a tabela "notas" e eu tenho a coluna "tipo". No db dump, recebo o seguinte: CONSTRAINT grades_type_check CHECK (((type) :: text = ANY ((ARRAY ['exam' :: variação de caracteres, 'test': : caractere variável, 'extra' :: caractere variável, 'intermediário' :: caractere variável, 'final' :: caractere variável]) :: text [])))
1
enum_type é apenas o seu próprio nome de tipo de enum @mariotanenbaum. Se sua enumeração é um "tipo", é isso que você deve usar.
precisa
26
é possível remover um?
Ced
8
Adicionando ao comentário do @DrewNoakes, se você estiver usando o db-migrate (que é executado na transação), poderá receber um erro: ERRO: ALTER TYPE ... ADD não pode ser executado dentro de um bloco de transação A solução é mencionada aqui (por Hubbitus ): stackoverflow.com/a/41696273/1161370
Mahesh
1
você não pode removê-lo, tornando impossível a migração da dow, portanto, é necessário recorrer a outros métodos
Muhammad Umer
65

Uma solução possível é a seguinte; pré-condição é que não haja conflitos nos valores de enum usados. (por exemplo, ao remover um valor de enumeração, verifique se esse valor não é mais usado.)

-- rename the old enum
alter type my_enum rename to my_enum__;
-- create the new enum
create type my_enum as enum ('value1', 'value2', 'value3');

-- alter all you enum columns
alter table my_table
  alter column my_column type my_enum using my_column::text::my_enum;

-- drop the old enum
drop type my_enum__;

Além disso, a ordem das colunas não será alterada.

Steffen
fonte
1
+1 é o caminho a seguir antes da 9.1 e ainda o caminho a seguir para excluir ou modificar elementos.
Essa é de longe a melhor resposta para minha solução, que adiciona novas enumerações a um tipo de enumeração existente, onde mantemos todas as enumerações antigas e adicionamos novas. Além disso, nosso script de atualização é transacional. Ótimo post!
precisa
1
Resposta brilhante! Evita hacks pg_enumque podem realmente quebrar as coisas e são transacionais, ao contrário ALTER TYPE ... ADD.
NathanAldenSr
4
No caso de sua coluna tem um valor padrão, você receberá o seguinte erro: default for column "my_column" cannot be cast automatically to type "my_enum". Você deverá fazer o seguinte: ALTER TABLE "my_table" ALTER COLUMN "my_column" DROP DEFAULT, ALTER COLUMN "my_column" TYPE "my_type" USING ("my_column"::text::"my_type"), ALTER COLUMN "my_column" SET DEFAULT 'my_default_value';
n1ru4l 15/03/19
30

Se você se enquadra na situação em que deve adicionar enumvalores na transação, executá-lo na migração flyway na ALTER TYPEinstrução, você receberá um erro ERROR: ALTER TYPE ... ADD cannot run inside a transaction block(consulte o número de flyway nº 350 ). Você pode adicionar esses valores pg_enumdiretamente como solução alternativa ( type_egais_unitsé o nome do destino enum):

INSERT INTO pg_enum (enumtypid, enumlabel, enumsortorder)
    SELECT 'type_egais_units'::regtype::oid, 'NEW_ENUM_VALUE', ( SELECT MAX(enumsortorder) + 1 FROM pg_enum WHERE enumtypid = 'type_egais_units'::regtype )
Hubbitus
fonte
9
No entanto, isso exigirá a concessão de permissões de administrador, pois altera a tabela do sistema.
Asnelzin
22

Complementando @Dariusz 1

Para o Rails 4.2.1, há esta seção de documento:

== Migrações transacionais

Se o adaptador de banco de dados suportar transações DDL, todas as migrações serão agrupadas automaticamente em uma transação. Existem consultas que você não pode executar dentro de uma transação e, para essas situações, pode desativar as transações automáticas.

class ChangeEnum < ActiveRecord::Migration
  disable_ddl_transaction!

  def up
    execute "ALTER TYPE model_size ADD VALUE 'new_value'"
  end
end
Kiko Castro
fonte
3
isto! se você está jogando com enums em trilhos modernos, é exatamente isso que você está procurando.
Eli Albert
1
Ótimo, me ajudou muito!
Dmytro Uhnichenko 23/10/19
10

Da documentação do Postgres 9.1 :

ALTER TYPE name ADD VALUE new_enum_value [ { BEFORE | AFTER } existing_enum_value ]

Exemplo:

ALTER TYPE user_status ADD VALUE 'PROVISIONAL' AFTER 'NORMAL'
Peymankh
fonte
3
Também da documentação: as comparações que envolvem um valor de enum adicionado às vezes serão mais lentas do que as comparações que envolvem apenas membros originais do tipo de enum. [.... detalhado cortado por muito tempo para comentar o fluxo de pilha ...] A desaceleração é geralmente insignificante; mas, se isso importa, o desempenho ideal pode ser recuperado eliminando e recriando o tipo de enum ou descartando e recarregando o banco de dados.
Aaron Zinman
8

Disclaimer: Eu não tentei esta solução, por isso pode não funcionar ;-)

Você deveria estar olhando pg_enum. Se você deseja alterar apenas o rótulo de um ENUM existente, uma simples UPDATE fará isso.

Para adicionar um novo valor ENUM:

  • Primeiro insira o novo valor em pg_enum . Se o novo valor tiver que ser o último, você está pronto.
  • Caso contrário (você precisará de um novo valor ENUM entre os existentes), será necessário atualizar cada valor distinto da tabela, passando do mais alto para o mais baixo ...
  • Depois, basta renomeá-los na pg_enumordem oposta.

Ilustração
Você tem o seguinte conjunto de etiquetas:

ENUM ('enum1', 'enum2', 'enum3')

e você deseja obter:

ENUM ('enum1', 'enum1b', 'enum2', 'enum3')

então:

INSERT INTO pg_enum (OID, 'newenum3');
UPDATE TABLE SET enumvalue TO 'newenum3' WHERE enumvalue='enum3';
UPDATE TABLE SET enumvalue TO 'enum3' WHERE enumvalue='enum2';

então:

UPDATE TABLE pg_enum SET name='enum1b' WHERE name='enum2' AND enumtypid=OID;

E assim por diante...

benja
fonte
5

Não consigo postar um comentário, então vou apenas dizer que a atualização do pg_enum funciona no Postgres 8.4. Para a maneira como nossas enumerações são configuradas, adicionamos novos valores aos tipos de enumerações existentes através de:

INSERT INTO pg_enum (enumtypid, enumlabel)
  SELECT typelem, 'NEWENUM' FROM pg_type WHERE
    typname = '_ENUMNAME_WITH_LEADING_UNDERSCORE';

É um pouco assustador, mas faz sentido, dada a maneira como o Postgres realmente armazena seus dados.

Josiah
fonte
1
Ótima resposta! Ajuda apenas para anexar uma nova enumeração, mas obviamente não resolve o caso de onde você deve solicitar novamente.
Mahmoud Abdelkader
Juntamente com o sublinhado principal para o nome do tipo, eles também diferenciam maiúsculas de minúsculas. Eu quase perdi a cabeça tentando selecionar por typename da tabela pg_type.
Mahesh
5

A atualização de pg_enum funciona, assim como o truque da coluna intermediária destacado acima. Pode-se também usar USING magic para alterar diretamente o tipo da coluna:

CREATE TYPE test AS enum('a', 'b');
CREATE TABLE foo (bar test);
INSERT INTO foo VALUES ('a'), ('b');

ALTER TABLE foo ALTER COLUMN bar TYPE varchar;

DROP TYPE test;
CREATE TYPE test as enum('a', 'b', 'c');

ALTER TABLE foo ALTER COLUMN bar TYPE test
USING CASE
WHEN bar = ANY (enum_range(null::test)::varchar[])
THEN bar::test
WHEN bar = ANY ('{convert, these, values}'::varchar[])
THEN 'c'::test
ELSE NULL
END;

Contanto que você não tenha funções que exijam explicitamente ou retornem essa enumeração, você é bom. (O pgsql reclamará quando você soltar o tipo, se houver.)

Além disso, observe que o PG9.1 está introduzindo uma instrução ALTER TYPE, que funcionará em enumerações:

http://developer.postgresql.org/pgdocs/postgres/release-9-1-alpha.html

Denis de Bernardy
fonte
A documentação relevante para PostgreSQL 9.1 agora pode ser encontrada em postgresql.org/docs/9.1/static/sql-altertype.html
Wichert Akkerman
1
ALTER TABLE foo ALTER COLUMN bar TYPE test USING bar::text::new_type;Mas em grande parte irrelevante agora ...
Erwin Brandstetter
Da mesma forma que Erwin disse, ... USING bar::typefuncionou para mim. Eu nem precisei especificar ::text.
Daniel Werner
3

Mais simples: livre-se das enumerações. Eles não são facilmente modificáveis ​​e, portanto, devem muito raramente ser usados.


fonte
2
talvez uma simples restrição de verificação faça?
1
E qual é exatamente o problema de armazenar valores como strings?
5
@ Grazer: na versão 9.1, você pode adicionar valores ao enum ( depesz.com/index.php/2010/10/27/… ) - mas ainda não é possível remover os antigos.
3
@WillSheppard - Eu acho que, basicamente, não. Eu acho que tipos personalizados baseados em texto com restrições de verificação são muito melhores em qualquer caso.
3
@JackDouglas - com certeza. Eu aceitaria domínio com cheque sobre enum qualquer dia.
3

Não é possível adicionar um comentário ao local apropriado, mas ALTER TABLE foo ALTER COLUMN bar TYPE new_enum_type USING bar::text::new_enum_typecom um padrão na coluna falhou. Eu precisei:

ALTER table ALTER COLUMN bar DROP DEFAULT;

e então funcionou.

Judy Morgan Loomis
fonte
3

por precaução, se você estiver usando o Rails e tiver várias instruções, precisará executar uma por uma, como:

execute "ALTER TYPE XXX ADD VALUE IF NOT EXISTS 'YYY';"
execute "ALTER TYPE XXX ADD VALUE IF NOT EXISTS 'ZZZ';"
edymerchk
fonte
1

Aqui está uma solução mais geral, mas bastante rápida, que além de alterar o tipo atualiza todas as colunas no banco de dados usando-o. O método pode ser aplicado mesmo que uma nova versão do ENUM seja diferente em mais de um rótulo ou perca alguns dos originais. O código abaixo substitui my_schema.my_type AS ENUM ('a', 'b', 'c')por ENUM ('a', 'b', 'd', 'e'):

CREATE OR REPLACE FUNCTION tmp() RETURNS BOOLEAN AS
$BODY$

DECLARE
    item RECORD;

BEGIN

    -- 1. create new type in replacement to my_type
    CREATE TYPE my_schema.my_type_NEW
        AS ENUM ('a', 'b', 'd', 'e');

    -- 2. select all columns in the db that have type my_type
    FOR item IN
        SELECT table_schema, table_name, column_name, udt_schema, udt_name
            FROM information_schema.columns
            WHERE
                udt_schema   = 'my_schema'
            AND udt_name     = 'my_type'
    LOOP
        -- 3. Change the type of every column using my_type to my_type_NEW
        EXECUTE
            ' ALTER TABLE ' || item.table_schema || '.' || item.table_name
         || ' ALTER COLUMN ' || item.column_name
         || ' TYPE my_schema.my_type_NEW'
         || ' USING ' || item.column_name || '::text::my_schema.my_type_NEW;';
    END LOOP;

    -- 4. Delete an old version of the type
    DROP TYPE my_schema.my_type;

    -- 5. Remove _NEW suffix from the new type
    ALTER TYPE my_schema.my_type_NEW
        RENAME TO my_type;

    RETURN true;

END
$BODY$
LANGUAGE 'plpgsql';

SELECT * FROM tmp();
DROP FUNCTION tmp();

Todo o processo será executado rapidamente, porque se a ordem dos rótulos persistir, nenhuma mudança real de dados ocorrerá. Eu apliquei o método em 5 tabelas usando my_typee tendo 50.000 a 70.000 linhas em cada uma, e todo o processo levou apenas 10 segundos.

Obviamente, a função retornará uma exceção no caso de rótulos ausentes na nova versão do ENUM serem usados ​​em algum lugar nos dados, mas nessa situação algo deve ser feito de qualquer maneira com antecedência.

Alexander Kachkaev
fonte
Isso é realmente valioso. O problema está nas visualizações usando o antigo ENUM, no entanto. Eles devem ser descartados e recriados, o que é muito mais complicado, considerando outras visualizações, dependendo das que foram descartadas. Não estou falando de tipos compostos ...
Ondřej Bouda
1

Para quem procura uma solução em transação, o seguinte parece funcionar.

Em vez de um ENUM, a DOMAINdeve ser usado no tipo TEXTcom uma restrição, verificando se o valor está dentro da lista especificada de valores permitidos (conforme sugerido por alguns comentários). O único problema é que nenhuma restrição pode ser adicionada (e, portanto, nem modificada) a um domínio se for usada por qualquer tipo composto (os documentos dizem apenas que "isso deve ser melhorado"). Essa restrição pode ser contornada, no entanto, usando uma restrição que chama uma função, da seguinte maneira.

START TRANSACTION;

CREATE FUNCTION test_is_allowed_label(lbl TEXT) RETURNS BOOL AS $function$
    SELECT lbl IN ('one', 'two', 'three');
$function$ LANGUAGE SQL IMMUTABLE;

CREATE DOMAIN test_domain AS TEXT CONSTRAINT val_check CHECK (test_is_allowed_label(value));

CREATE TYPE test_composite AS (num INT, word test_domain);

CREATE TABLE test_table (val test_composite);
INSERT INTO test_table (val) VALUES ((1, 'one')::test_composite), ((3, 'three')::test_composite);
-- INSERT INTO test_table (val) VALUES ((4, 'four')::test_composite); -- restricted by the CHECK constraint

CREATE VIEW test_view AS SELECT * FROM test_table; -- just to show that the views using the type work as expected

CREATE OR REPLACE FUNCTION test_is_allowed_label(lbl TEXT) RETURNS BOOL AS $function$
    SELECT lbl IN ('one', 'two', 'three', 'four');
$function$ LANGUAGE SQL IMMUTABLE;

INSERT INTO test_table (val) VALUES ((4, 'four')::test_composite); -- allowed by the new effective definition of the constraint

SELECT * FROM test_view;

CREATE OR REPLACE FUNCTION test_is_allowed_label(lbl TEXT) RETURNS BOOL AS $function$
    SELECT lbl IN ('one', 'two', 'three');
$function$ LANGUAGE SQL IMMUTABLE;

-- INSERT INTO test_table (val) VALUES ((4, 'four')::test_composite); -- restricted by the CHECK constraint, again

SELECT * FROM test_view; -- note the view lists the restricted value 'four' as no checks are made on existing data

DROP VIEW test_view;
DROP TABLE test_table;
DROP TYPE test_composite;
DROP DOMAIN test_domain;
DROP FUNCTION test_is_allowed_label(TEXT);

COMMIT;

Anteriormente, usei uma solução semelhante à resposta aceita, mas está longe de ser boa quando são consideradas visualizações ou funções ou tipos compostos (e principalmente visualizações usando outras visualizações usando os ENUMs modificados ...). A solução proposta nesta resposta parece funcionar sob quaisquer condições.

A única desvantagem é que nenhuma verificação é executada nos dados existentes quando alguns valores permitidos são removidos (o que pode ser aceitável, especialmente para esta pergunta). ALTER DOMAIN test_domain VALIDATE CONSTRAINT val_checkInfelizmente, uma chamada termina com o mesmo erro que adicionar uma nova restrição ao domínio usado por um tipo composto.

Observe que uma leve modificação, como CHECK (value = ANY(get_allowed_values()))onde a get_allowed_values()função retornou a lista de valores permitidos, não funcionaria - o que é bastante estranho, então espero que a solução proposta acima funcione de maneira confiável (funciona para mim até agora ...). (funciona, na verdade - foi o meu erro)

Ondřej Bouda
fonte
0

Como discutido acima, o ALTERcomando não pode ser gravado dentro de uma transação. A maneira sugerida é inserir diretamente na tabela pg_enum, por retrieving the typelem from pg_type tableecalculating the next enumsortorder number ;

A seguir está o código que eu uso. (Verifica se existe um valor duplicado antes da inserção (restrição entre o nome da enumtypid e a enumlabel)

INSERT INTO pg_enum (enumtypid, enumlabel, enumsortorder)
    SELECT typelem,
    'NEW_ENUM_VALUE',
    (SELECT MAX(enumsortorder) + 1 
        FROM pg_enum e
        JOIN pg_type p
        ON p.typelem = e.enumtypid
        WHERE p.typname = '_mytypename'
    )
    FROM pg_type p
    WHERE p.typname = '_mytypename'
    AND NOT EXISTS (
        SELECT * FROM 
        pg_enum e
        JOIN pg_type p
        ON p.typelem = e.enumtypid
        WHERE e.enumlabel = 'NEW_ENUM_VALUE'
        AND p.typname = '_mytypename'
    )

Observe que o nome do seu tipo é anexado com um sublinhado na tabela pg_type. Além disso, o nome do tipo deve estar em letras minúsculas na cláusula where.

Agora isso pode ser gravado com segurança no seu script db migrate.

Mahesh
fonte
-1

Não sei se tenho outra opção, mas podemos diminuir o valor usando:

select oid from pg_type where typname = 'fase';'
select * from pg_enum where enumtypid = 24773;'
select * from pg_enum where enumtypid = 24773 and enumsortorder = 6;
delete from pg_enum where enumtypid = 24773 and enumsortorder = 6;
Jardel
fonte
-2

Ao usar o Navicat, você pode acessar os tipos (em exibição -> outros -> tipos) - obter a visualização do design do tipo - e clicar no botão "adicionar rótulo".

jvv
fonte
1
Seria bom, mas na vida real, não é útil:ERROR: cannot drop type foo because other objects depend on it HINT: Use DROP ... CASCADE to drop the dependent objects too.
Ortwin Gentz
Estranho, funcionou para mim. (Não sei por que você usa GOTA quando TS só queria adicionar um valor para o campo enum)
JVV
1
Não fiz uma DROP especificamente, mas fui exatamente após o procedimento. Presumo que a Navicat faça a DROP nos bastidores e falhe. Estou usando o Navicat 9.1.5 Lite.
Ortwin Gentz