Ajout d'une nouvelle valeur à un type ENUM existant

208

J'ai une colonne de table qui utilise un enumtype. Je souhaite mettre à jour ce enumtype pour avoir une valeur supplémentaire possible. Je ne veux supprimer aucune valeur existante, ajoutez simplement la nouvelle valeur. Quelle est la manière la plus simple de procéder?

Ian
la source

Réponses:

153

REMARQUE si vous utilisez PostgreSQL 9.1 ou une version ultérieure et que vous êtes d'accord pour effectuer des modifications en dehors d'une transaction, consultez cette réponse pour une approche plus simple.


J'ai eu le même problème il y a quelques jours et j'ai trouvé ce message. Donc ma réponse peut être utile pour quelqu'un qui cherche une solution :)

Si vous n'avez qu'une ou deux colonnes qui utilisent le type d'énumération que vous souhaitez modifier, vous pouvez essayer ceci. Vous pouvez également modifier l'ordre des valeurs dans le nouveau type.

-- 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 doit être répété s'il y a plus d'une colonne.

taksofan
la source
9
Il convient de mentionner que tout cela peut être effectué en une seule transaction, il est donc généralement sûr de le faire dans une base de données de production.
David Leppik
52
Ce n'était jamais une bonne idée. Depuis la version 9.1, vous pouvez tout faire avec ALTER TYPE. Mais même avant cela, ALTER TABLE foo ALTER COLUMN bar TYPE new_type USING bar::text::new_type;était de loin supérieur.
Erwin Brandstetter
1
Sachez que les anciennes versions de Postgres ne prennent pas en charge les types de changement de nom. Plus précisément, la version de Postgres sur Heroku (base de données partagée, je crois qu'ils utilisent PG 8.3) ne la prend pas en charge.
Ortwin Gentz
13
Vous pouvez regrouper les étapes 3, 4, 5 et 6 en une seule déclaration:ALTER TABLE some_table ALTER COLUMN some_column TYPE some_enum_type USING some_column::text::some_enum_type;
glyphobet
3
Si vous faites cela sur une table en direct, verrouillez la table pendant la procédure. Le niveau d'isolement des transactions par défaut dans postgresql n'empêchera pas l'insertion de nouvelles lignes par d'autres transactions au cours de cette transaction, vous risquez donc de vous retrouver avec des lignes mal remplies.
Sérgio Carvalho
422

PostgreSQL 9.1 introduit la capacité des types 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
la source
1
qu'est-ce que le "enum_type"? nom de champ, nom de champ_table? ou autre chose? comment dois-je frapper ça? J'ai une table "grades" et j'ai une colonne "type" Et dans db dump j'obtiens ceci: CONSTRAINT grades_type_check CHECK (((type) :: text = ANY ((ARRAY ['exam' :: character variant, 'test': : caractère variable, 'extra' :: caractère variable, 'midterm' :: caractère variable, 'final' :: caractère variable]) :: text [])))
1
enum_type est juste votre propre nom de type enum @mariotanenbaum. Si votre énumération est un "type", c'est ce que vous devez utiliser.
Dariusz
26
est-il possible d'en supprimer un?
Ced
8
Ajout au commentaire de @DrewNoakes, si vous utilisez db-migrate (qui s'exécute en transaction), vous pouvez obtenir une erreur: ERREUR: ALTER TYPE ... ADD ne peut pas s'exécuter dans un bloc de transaction La solution est mentionnée ici (par Hubbitus ): stackoverflow.com/a/41696273/1161370
Mahesh
1
vous ne pouvez pas le supprimer, ce qui rend la migration de Dow impossible, alors vous devez recourir à d'autres méthodes
Muhammad Umer
65

Une solution possible est la suivante; la condition préalable est qu'il n'y ait pas de conflits dans les valeurs énumérées utilisées. (Par exemple, lors de la suppression d'une valeur d'énumération, assurez-vous que cette valeur n'est plus utilisée.)

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

De cette manière également, l'ordre des colonnes ne sera pas modifié.

Steffen
la source
1
+1 c'est le chemin à parcourir avant la 9.1 et toujours le chemin à parcourir pour supprimer ou modifier des éléments.
C'est de loin la meilleure réponse pour ma solution, qui ajoute de nouvelles énumérations à un type d'énumération existant, où nous conservons toutes les anciennes énumérations et en ajoutons de nouvelles. De plus, notre script de mise à jour est transactionnel. Très bonne publication!
Darin Peterson
1
Réponse brillante! Évite les hacks autour pg_enumqui peuvent réellement casser des choses et sont transactionnels, contrairement à ALTER TYPE ... ADD.
NathanAldenSr
4
Dans le cas où votre colonne a une valeur par défaut , vous recevrez l'erreur suivante: default for column "my_column" cannot be cast automatically to type "my_enum". Vous devrez faire ce qui suit: 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 vous tombez dans une situation où vous devez ajouter des enumvaleurs dans la transaction, par exemple, exécutez-la lors de la migration de la voie de migration sur l' ALTER TYPEinstruction, vous obtiendrez une erreur ERROR: ALTER TYPE ... ADD cannot run inside a transaction block(voir problème de voie de migration # 350 ), vous pouvez ajouter ces valeurs pg_enumdirectement comme solution de contournement ( type_egais_unitsest le nom de la cible 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
la source
9
Cependant, cela nécessitera l'octroi d'autorisations d'administrateur, car cela modifie la table système.
asnelzin
22

Compléter @Dariusz 1

Pour Rails 4.2.1, il y a cette section doc:

== Migrations transactionnelles

Si l'adaptateur de base de données prend en charge les transactions DDL, toutes les migrations seront automatiquement encapsulées dans une transaction. Il existe cependant des requêtes que vous ne pouvez pas exécuter dans une transaction, et pour ces situations, vous pouvez désactiver les transactions automatiques.

class ChangeEnum < ActiveRecord::Migration
  disable_ddl_transaction!

  def up
    execute "ALTER TYPE model_size ADD VALUE 'new_value'"
  end
end
Kiko Castro
la source
3
ce! si vous jouez avec des énumérations dans des rails modernes, c'est exactement ce que vous recherchez.
Eli Albert
1
Super, ça m'a beaucoup aidé!
Dmytro Uhnichenko
10

Depuis la documentation Postgres 9.1 :

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

Exemple:

ALTER TYPE user_status ADD VALUE 'PROVISIONAL' AFTER 'NORMAL'
Peymankh
la source
3
Également à partir de la documentation: Les comparaisons impliquant une valeur ajoutée enum seront parfois plus lentes que les comparaisons impliquant uniquement des membres originaux du type enum. [.... détaillé coupé comme trop long pour le commentaire stackoverflow ...] Le ralentissement est généralement insignifiant; mais si cela est important, des performances optimales peuvent être récupérées en supprimant et en recréant le type d'énumération, ou en vidant et en rechargeant la base de données.
Aaron Zinman
8

Avis de non-responsabilité: je n'ai pas essayé cette solution, elle pourrait donc ne pas fonctionner ;-)

Vous devriez regarder pg_enum. Si vous voulez seulement changer l'étiquette d'un ENUM existant, une simple MISE À JOUR le fera.

Pour ajouter de nouvelles valeurs ENUM:

  • Insérez d'abord la nouvelle valeur dans pg_enum . Si la nouvelle valeur doit être la dernière, vous avez terminé.
  • Si ce n'est pas le cas (vous avez besoin d'une nouvelle valeur ENUM entre celles existantes), vous devrez mettre à jour chaque valeur distincte de votre table, en passant de la plus haute à la plus basse ...
  • Il vous suffira alors de les renommer pg_enumdans l'ordre inverse.

Illustration
Vous disposez du jeu d'étiquettes suivant:

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

et vous souhaitez obtenir:

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

puis:

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

puis:

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

Etc...

benja
la source
5

Je n'arrive pas à poster un commentaire, donc je vais juste dire que la mise à jour de pg_enum fonctionne dans Postgres 8.4. Pour la façon dont nos énumérations sont configurées, j'ai ajouté de nouvelles valeurs aux types d'énumérations existants via:

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

C'est un peu effrayant, mais cela a du sens étant donné la façon dont Postgres stocke réellement ses données.

Josiah
la source
1
Très bonne réponse! Aide simplement à ajouter une nouvelle énumération, mais ne résout évidemment pas le cas où vous devez passer une nouvelle commande.
Mahmoud Abdelkader
Avec le trait de soulignement principal pour le nom de type, ils sont également sensibles à la casse. J'ai presque perdu la tête en essayant de sélectionner par nom de type dans la table pg_type.
Mahesh
5

La mise à jour de pg_enum fonctionne, tout comme l'astuce de colonne intermédiaire mise en évidence ci-dessus. On peut également utiliser USING magic pour changer directement le type de la colonne:

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;

Tant que vous n'avez aucune fonction qui requiert ou renvoie explicitement cette énumération, vous êtes bon. (pgsql se plaindra lorsque vous supprimez le type, le cas échéant.)

Notez également que PG9.1 introduit une instruction ALTER TYPE, qui fonctionnera sur les énumérations:

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

Denis de Bernardy
la source
La documentation pertinente pour PostgreSQL 9.1 peut maintenant être trouvée sur 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;Mais largement hors de propos maintenant ...
Erwin Brandstetter
De la même manière que ce qu'a dit Erwin, cela a ... USING bar::typefonctionné pour moi. Je n'avais même pas besoin de préciser ::text.
Daniel Werner
3

Le plus simple: se débarrasser des énumérations. Ils ne sont pas facilement modifiables et devraient donc très rarement être utilisés.


la source
2
peut-être qu'une simple contrainte de vérification fera l'affaire?
1
Et quel est exactement le problème du stockage des valeurs sous forme de chaînes?
5
@Grazer: dans 9.1, vous pouvez ajouter des valeurs à enum ( depesz.com/index.php/2010/10/27/… ) - mais vous ne pouvez toujours pas supprimer les anciennes.
3
@ WillSheppard - Je pense que, fondamentalement, jamais. Je pense que les types personnalisés basés sur du texte avec des contraintes de vérification sont beaucoup mieux dans tous les cas.
3
@JackDouglas - bien sûr. Je prendrais le domaine avec une vérification de l'énumération chaque jour.
3

Impossible d'ajouter un commentaire à l'endroit approprié, mais ALTER TABLE foo ALTER COLUMN bar TYPE new_enum_type USING bar::text::new_enum_typeavec une valeur par défaut sur la colonne a échoué. J'ai dû:

ALTER table ALTER COLUMN bar DROP DEFAULT;

et puis ça a marché.

Judy Morgan Loomis
la source
3

juste au cas où, si vous utilisez Rails et que vous avez plusieurs instructions, vous devrez les exécuter une par une, comme:

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

Voici une solution plus générale mais plutôt rapide, qui à part changer le type lui-même met à jour toutes les colonnes de la base de données l'utilisant. La méthode peut être appliquée même si une nouvelle version d'ENUM est différente par plus d'une étiquette ou manque certaines des originales. Le code ci-dessous remplace my_schema.my_type AS ENUM ('a', 'b', 'c')par 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();

L'ensemble du processus se déroulera assez rapidement, car si l'ordre des étiquettes persiste, aucun changement réel des données ne se produira. J'ai appliqué la méthode sur 5 tables en utilisant my_typeet ayant 50 000 à 70 000 lignes dans chacune, et l'ensemble du processus n'a pris que 10 secondes.

Bien sûr, la fonction renverra une exception au cas où des étiquettes manquantes dans la nouvelle version de l'ENUM sont utilisées quelque part dans les données, mais dans une telle situation, quelque chose devrait être fait au préalable de toute façon.

Alexander Kachkaev
la source
C'est vraiment précieux. Le problème vient des vues utilisant l'ancien ENUM. Ils doivent être supprimés et recréés, ce qui est beaucoup plus compliqué compte tenu des autres vues en fonction des vues supprimées. Sans parler des types composites ...
Ondřej Bouda
1

Pour ceux qui recherchent une solution en transaction, ce qui suit semble fonctionner.

Au lieu de an ENUM, a DOMAINdoit être utilisé sur type TEXTavec une contrainte vérifiant que la valeur se trouve dans la liste spécifiée des valeurs autorisées (comme suggéré par certains commentaires). Le seul problème est qu'aucune contrainte ne peut être ajoutée (et donc ni modifiée) à un domaine s'il est utilisé par n'importe quel type composite (la documentation dit simplement que cela "devrait éventuellement être amélioré"). Une telle restriction peut cependant être contournée en utilisant une contrainte appelant une fonction, comme suit.

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;

Auparavant, j'ai utilisé une solution similaire à la réponse acceptée, mais elle est loin d'être bonne une fois que les vues ou les fonctions ou les types composites (et en particulier les vues utilisant d'autres vues utilisant les ENUM modifiés ...) sont considérés. La solution proposée dans cette réponse semble fonctionner dans toutes les conditions.

Le seul inconvénient est qu'aucune vérification n'est effectuée sur les données existantes lorsque certaines valeurs autorisées sont supprimées (ce qui pourrait être acceptable, en particulier pour cette question). (Un appel à ALTER DOMAIN test_domain VALIDATE CONSTRAINT val_checkaboutit à la même erreur que l'ajout d'une nouvelle contrainte au domaine utilisé par un type composite, malheureusement.)

Notez qu'une légère modification telle que CHECK (value = ANY(get_allowed_values())), lorsque la get_allowed_values()fonction a renvoyé la liste des valeurs autorisées, ne fonctionnerait pas - ce qui est assez étrange, donc j'espère que la solution proposée ci-dessus fonctionne de manière fiable (elle le fait pour moi, jusqu'à présent ...). (ça marche, en fait - c'était mon erreur)

Ondřej Bouda
la source
0

Comme indiqué ci-dessus, la ALTERcommande ne peut pas être écrite dans une transaction. La méthode suggérée consiste à insérer directement dans la table pg_enum, par retrieving the typelem from pg_type tableetcalculating the next enumsortorder number ;

Voici le code que j'utilise. (Vérifie s'il existe une valeur en double avant l'insertion (contrainte entre enumtypid et 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'
    )

Notez que votre nom de type est précédé d'un trait de soulignement dans la table pg_type. En outre, le nom de type doit être entièrement en minuscules dans la clause where.

Maintenant, cela peut être écrit en toute sécurité dans votre script de migration db.

Mahesh
la source
-1

Je ne sais pas si j'ai une autre option mais nous pouvons laisser tomber la valeur en utilisant:

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
la source
-2

Lorsque vous utilisez Navicat, vous pouvez accéder aux types (sous Affichage -> Autres -> Types) - obtenir la vue de conception du type - et cliquer sur le bouton "Ajouter une étiquette".

jvv
la source
1
Ce serait bien mais dans la vraie vie, ce n'est pas utile:ERROR: cannot drop type foo because other objects depend on it HINT: Use DROP ... CASCADE to drop the dependent objects too.
Ortwin Gentz
Bizarre, ça a marché pour moi. (Je ne sais pas pourquoi vous utilisez DROP alors que TS ne voulait ajouter qu'une valeur au champ enum)
jvv
1
Je n'ai pas fait de DROP spécifiquement, mais je suis allé exactement après votre procédure. Je suppose que Navicat fait le DROP dans les coulisses et échoue. J'utilise Navicat 9.1.5 Lite.
Ortwin Gentz