Agregar un nuevo valor a un tipo ENUM existente

208

Tengo una columna de tabla que usa un enumtipo. Deseo actualizar ese enumtipo para tener un valor adicional posible. No quiero eliminar ningún valor existente, solo agregue el nuevo valor. ¿Cuál es la forma más sencilla de hacer esto?

Ian
fuente

Respuestas:

153

NOTA: si está utilizando PostgreSQL 9.1 o posterior, y está de acuerdo con hacer cambios fuera de una transacción, vea esta respuesta para un enfoque más simple.


Tuve el mismo problema hace unos días y encontré esta publicación. Entonces mi respuesta puede ser útil para alguien que está buscando una solución :)

Si solo tiene una o dos columnas que usan el tipo de enumeración que desea cambiar, puede intentarlo. También puede cambiar el orden de los valores en el nuevo 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 debe repetirse si hay más de 1 columna.

taksofan
fuente
9
Vale la pena mencionar que todo esto se puede hacer en una sola transacción, por lo que es más seguro hacerlo en una base de datos de producción.
David Leppik
52
Esto nunca fue una buena idea. Desde 9.1 puedes hacerlo todo con ALTER TYPE. Pero incluso antes de eso, ALTER TABLE foo ALTER COLUMN bar TYPE new_type USING bar::text::new_type;era muy superior.
Erwin Brandstetter
1
Tenga en cuenta que las versiones anteriores de Postgres no admiten tipos de cambio de nombre. Específicamente, la versión de Postgres en Heroku (db compartido, creo que usan PG 8.3) no es compatible.
Ortwin Gentz
13
Puede contraer los pasos 3, 4, 5 y 6 en una sola declaración:ALTER TABLE some_table ALTER COLUMN some_column TYPE some_enum_type USING some_column::text::some_enum_type;
glyphobet
3
Si hace esto en una mesa en vivo, bloquee la mesa durante el procedimiento. El nivel de aislamiento de transacción predeterminado en postgresql no evitará que otras transacciones inserten nuevas filas durante esta transacción, por lo que puede quedar con filas rellenadas incorrectamente.
Sérgio Carvalho
422

PostgreSQL 9.1 introduce la capacidad de ALTERAR tipos de enumeración:

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
fuente
1
¿Qué es el "enum_type"? nombre de campo, nombre de tabla_campo? ¿o algo mas? ¿Cómo debería golpear eso? Tengo la tabla "grados" y tengo la columna "tipo" Y en db dump obtengo esto: CONSTRAINT grade_type_check CHECK (((type) :: text = ANY ((ARRAY ['examen' :: carácter variable, 'prueba': : variación de caracteres, 'extra' :: variación de caracteres, 'medio término' :: variación de caracteres, 'final' :: variación de caracteres]) :: texto [])))
1
enum_type es solo un nombre de tipo de enumeración propio @mariotanenbaum. Si su enumeración es un "tipo", entonces esto es lo que debe usar.
Dariusz
26
¿Es posible eliminar uno?
Ced
8
Agregando al comentario de @DrewNoakes, si está utilizando db-migrate (que se ejecuta en una transacción), entonces puede obtener un error: ERROR: ALTER TYPE ... ADD no se puede ejecutar dentro de un bloque de transacción La solución se menciona aquí (por Hubbitus ): stackoverflow.com/a/41696273/1161370
Mahesh el
1
no puede eliminarlo, por lo que hace imposible la migración por dow, así que tenga que recurrir a otros métodos
Muhammad Umer
65

Una posible solución es la siguiente; La condición previa es que no hay conflictos en los valores de enumeración utilizados. (por ejemplo, al eliminar un valor de enumeración, asegúrese de que este valor ya no se use).

-- 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__;

También de esta manera no se cambiará el orden de las columnas.

Steffen
fuente
1
+1 este es el camino a seguir antes de 9.1 y sigue siendo el camino para eliminar o modificar elementos.
Esta es, con mucho, la mejor respuesta para mi solución, que agrega nuevas enumeraciones a un tipo de enumeración existente, donde mantenemos todas las enumeraciones antiguas y agregamos nuevas. Además, nuestro script de actualización es transaccional. ¡Buena publicación!
Darin Peterson
1
Respuesta brillante! Evita ataques pg_enumque realmente pueden romper cosas y es transaccional, a diferencia ALTER TYPE ... ADD.
NathanAldenSr
44
En caso de que su columna tiene un valor por defecto, recibirá el siguiente error: default for column "my_column" cannot be cast automatically to type "my_enum". Deberá hacer lo siguiente: 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
30

Si se encuentra en una situación en la que debe agregar enumvalores en la transacción, por ejemplo, ejecútelo en la migración de ruta de acceso en la ALTER TYPEinstrucción, recibirá un error ERROR: ALTER TYPE ... ADD cannot run inside a transaction block(vea el problema de ruta # 350 ) en el que podría agregar dichos valores pg_enumdirectamente como solución alternativa ( type_egais_unitses el nombre del objetivo 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
fuente
9
Sin embargo, esto requerirá otorgar permisos de administrador, ya que cambia la tabla del sistema.
asnelzin el
22

Complementando a @Dariusz 1

Para Rails 4.2.1, existe esta sección de documentación:

== Migraciones transaccionales

Si el adaptador de base de datos admite transacciones DDL, todas las migraciones se incluirán automáticamente en una transacción. Sin embargo, hay consultas que no puede ejecutar dentro de una transacción, y para estas situaciones puede desactivar las transacciones automáticas.

class ChangeEnum < ActiveRecord::Migration
  disable_ddl_transaction!

  def up
    execute "ALTER TYPE model_size ADD VALUE 'new_value'"
  end
end
Kiko Castro
fuente
3
¡esta! Si estás jugando con enumeraciones en rieles modernos, esto es exactamente lo que estás buscando.
Eli Albert
1
Genial, me ayudó mucho!
Dmytro Uhnichenko el
10

De la documentación de Postgres 9.1 :

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

Ejemplo:

ALTER TYPE user_status ADD VALUE 'PROVISIONAL' AFTER 'NORMAL'
Peymankh
fuente
3
También de la documentación: las comparaciones que involucran un valor de enumeración agregado a veces serán más lentas que las comparaciones que involucran solo miembros originales del tipo de enumeración. [... detallado recortado como demasiado largo para el comentario de stackoverflow ...] La desaceleración suele ser insignificante; pero si es importante, se puede recuperar el rendimiento óptimo soltando y recreando el tipo de enumeración, o volcando y volviendo a cargar la base de datos.
Aaron Zinman
8

Descargo de responsabilidad: no he probado esta solución, por lo que podría no funcionar ;-)

Deberías estar mirando pg_enum. Si solo desea cambiar la etiqueta de un ENUM existente, una simple ACTUALIZACIÓN lo hará.

Para agregar un nuevo valor ENUM:

  • Primero inserte el nuevo valor en pg_enum. Si el nuevo valor tiene que ser el último, ya está.
  • Si no (necesita un nuevo valor ENUM entre los existentes), deberá actualizar cada valor distinto en su tabla, pasando del más alto al más bajo ...
  • Entonces solo tendrás que cambiarles el nombre pg_enumen el orden opuesto.

Ilustración
Tiene el siguiente conjunto de etiquetas:

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

y quieres obtener:

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

luego:

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

luego:

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

Y así...

benja
fuente
5

Parece que no puedo publicar un comentario, así que solo diré que actualizar pg_enum funciona en Postgres 8.4. Por la forma en que están configuradas nuestras enumeraciones, he agregado nuevos valores a los tipos de enumeración existentes a través de:

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

Da un poco de miedo, pero tiene sentido dada la forma en que Postgres realmente almacena sus datos.

Josiah
fuente
1
¡Gran respuesta! Ayuda solo para agregar una nueva enumeración, pero obviamente no resuelve el caso en el que tiene que reordenar.
Mahmoud Abdelkader
Junto con el guión bajo principal para typename, también distinguen entre mayúsculas y minúsculas. Casi pierdo la cabeza tratando de seleccionar por tipo de nombre de la tabla pg_type.
Mahesh
5

La actualización de pg_enum funciona, al igual que el truco de la columna intermedia resaltado anteriormente. También se puede usar USING magic para cambiar el tipo de columna directamente:

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;

Mientras no tenga funciones que requieran o devuelvan explícitamente esa enumeración, está bien. (pgsql se quejará cuando suelte el tipo si lo hay).

Además, tenga en cuenta que PG9.1 está introduciendo una declaración ALTER TYPE, que funcionará en enumeraciones:

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

Denis de Bernardy
fuente
La documentación relevante para PostgreSQL 9.1 ahora se puede encontrar en 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;Pero en gran medida irrelevante ahora ...
Erwin Brandstetter
De manera similar a lo que dijo Erwin, ... USING bar::typefuncionó para mí. Ni siquiera tuve que especificar ::text.
Daniel Werner
3

Más simple: deshacerse de las enumeraciones. Ellos no son fácilmente modificables, y por lo tanto deben muy rara vez se pueden usar.


fuente
2
tal vez una simple restricción de verificación va a hacer?
1
¿Y cuál es exactamente el problema de almacenar valores como cadenas?
55
@Grazer: en 9.1 puede agregar valores a enum ( depesz.com/index.php/2010/10/27/… ), pero aún no puede eliminar los antiguos.
3
@WillSheppard - yo creo que en el fondo no. Creo que los tipos personalizados basados ​​en texto con restricciones de verificación son mucho mejores en cualquier caso.
3
@JackDouglas, claro. Tomaría dominio con check over enum cualquier día.
3

No se puede agregar un comentario al lugar apropiado, pero ALTER TABLE foo ALTER COLUMN bar TYPE new_enum_type USING bar::text::new_enum_typecon un valor predeterminado en la columna falló. Tuve que:

ALTER table ALTER COLUMN bar DROP DEFAULT;

Y luego funcionó.

Judy Morgan Loomis
fuente
3

por si acaso, si está utilizando Rails y tiene varias declaraciones, deberá ejecutar una por una, como:

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

Aquí hay una solución más general pero de trabajo bastante rápido, que además de cambiar el tipo en sí, actualiza todas las columnas de la base de datos que la usan. El método puede aplicarse incluso si una nueva versión de ENUM es diferente en más de una etiqueta o pierde algunas de las originales. El siguiente código reemplaza my_schema.my_type AS ENUM ('a', 'b', 'c')con 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 el proceso se ejecutará con bastante rapidez, porque si el orden de las etiquetas persiste, no ocurrirá ningún cambio real de datos. Apliqué el método en 5 tablas usando my_typey teniendo 50,000-70,000 filas en cada una, y todo el proceso tomó solo 10 segundos.

Por supuesto, la función devolverá una excepción en caso de que las etiquetas que faltan en la nueva versión de ENUM se usen en algún lugar de los datos, pero en tal situación se debe hacer algo de antemano de todos modos.

Alexander Kachkaev
fuente
Esto es realmente valioso. Sin embargo, el problema es con las vistas que usan el antiguo ENUM. Deben descartarse y recrearse, lo cual es mucho más complicado teniendo en cuenta otras vistas dependiendo de las eliminadas. No estoy hablando de tipos compuestos ...
Ondřej Bouda
1

Para aquellos que buscan una solución en la transacción, lo siguiente parece funcionar.

En lugar de un ENUM, DOMAINse utilizará un tipo TEXTcon una restricción que verifica que el valor esté dentro de la lista especificada de valores permitidos (como sugieren algunos comentarios). El único problema es que no se puede agregar ninguna restricción (y, por lo tanto, tampoco modificarla) a un dominio si es utilizada por cualquier tipo compuesto (los documentos simplemente dicen que "eventualmente debería mejorarse"). Sin embargo, tal restricción se puede solucionar utilizando una restricción que llame a una función, como se indica a continuación.

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, utilicé una solución similar a la respuesta aceptada, pero está lejos de ser buena una vez que se consideran las vistas o funciones o tipos compuestos (y especialmente las vistas que usan otras vistas que usan los ENUM modificados ...). La solución propuesta en esta respuesta parece funcionar bajo cualquier condición.

La única desventaja es que no se realizan verificaciones en los datos existentes cuando se eliminan algunos valores permitidos (lo que podría ser aceptable, especialmente para esta pregunta). (Una llamada a ALTER DOMAIN test_domain VALIDATE CONSTRAINT val_checktermina con el mismo error que agregar una nueva restricción al dominio utilizado por un tipo compuesto, desafortunadamente).

Tenga en cuenta que una ligera modificación como CHECK (value = ANY(get_allowed_values())), por ejemplo , donde la get_allowed_values()función devolvió la lista de valores permitidos, no funcionaría, lo cual es bastante extraño, por lo que espero que la solución propuesta anteriormente funcione de manera confiable (lo hace para mí, hasta ahora ...). (funciona, de hecho, fue mi error)

Ondřej Bouda
fuente
0

Como se discutió anteriormente, el ALTERcomando no se puede escribir dentro de una transacción. La forma sugerida es insertar directamente en la tabla pg_enum, por retrieving the typelem from pg_type tabley calculating the next enumsortorder number;

El siguiente es el código que uso. (Comprueba si existe un valor duplicado antes de insertar (restricción entre enumtypid y enumlabel name)

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'
    )

Tenga en cuenta que su nombre de tipo se antepone con un guión bajo en la tabla pg_type. Además, el nombre típico debe estar en minúsculas en la cláusula where.

Ahora esto se puede escribir de forma segura en su secuencia de comandos db migrate.

Mahesh
fuente
-1

No sé si tiene otra opción, pero podemos soltar el 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
fuente
-2

Al usar Navicat, puede ir a tipos (en vista -> otros -> tipos), obtener la vista de diseño del tipo y hacer clic en el botón "agregar etiqueta".

jvv
fuente
1
Sería bueno, pero en la vida real, no es útil:ERROR: cannot drop type foo because other objects depend on it HINT: Use DROP ... CASCADE to drop the dependent objects too.
Ortwin Gentz
Extraño, funcionó para mí. (No estoy seguro de por qué usa DROP cuando TS solo quería agregar un valor al campo enum)
jvv
1
No hice un DROP específicamente, pero fui exactamente después de su procedimiento. Supongo que Navicat hace el DROP detrás de escena y falla. Estoy usando Navicat 9.1.5 Lite.
Ortwin Gentz