web-dev-qa-db-fra.com

Ajout d'une nouvelle valeur à un type ENUM existant

J'ai une colonne de table qui utilise un type enum. Je souhaite mettre à jour ce type enum pour avoir une valeur supplémentaire possible. Je ne veux pas supprimer les valeurs existantes, ajoutez simplement la nouvelle valeur. Quel est le moyen le plus simple de faire cela?

164
Ian

[~ # ~] notez [~ # ~] si vous utilisez PostgreSQL 9.1 ou une version ultérieure, et que vous êtes en mesure d'effectuer des modifications en dehors d'une transaction. , voir 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 post. Donc, ma réponse peut être utile pour quelqu'un qui cherche une solution :)

Si vous avez seulement une ou deux colonnes qui utilisent le type d’énum que vous voulez changer, 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.

130
taksofan

PostgreSQL 9.1 introduit la capacité à MODIFIER Types énumérés:

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';
339
Dariusz

Une solution possible est la suivante. La condition préalable est qu’il n’y ait pas de conflits dans les valeurs d’enum utilisées. (Par exemple, lorsque vous supprimez une valeur enum, 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 la même manière, l'ordre des colonnes ne sera pas modifié.

60
Steffen

Si vous vous trouvez dans une situation où vous devriez ajouter enum valeurs dans une transaction, par ex. exécutez-le dans la migration de la voie de migration sur ALTER TYPE déclaration vous obtiendrez l'erreur ERROR: ALTER TYPE ... ADD cannot run inside a transaction block (voir numéro de la voie de migration # 35 ) vous pouvez ajouter de telles valeurs dans pg_enum directement comme solution de contournement (type_egais_units est 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 )
27
Hubbitus

En complément de @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 vous pouvez désactiver les transactions automatiques dans ces situations.

class ChangeEnum < ActiveRecord::Migration
  disable_ddl_transaction!

  def up
    execute "ALTER TYPE model_size ADD VALUE 'new_value'"
  end
end
13
Kiko Castro

De Postgres 9.1 Documentation :

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

Exemple:

ALTER TYPE user_status ADD VALUE 'PROVISIONAL' AFTER 'NORMAL'
10
Peymankh

Disclaimer: Je n'ai pas essayé cette solution, donc cela pourrait ne pas fonctionner ;-)

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

Pour ajouter une nouvelle valeur ENUM:

  • Commencez par insérer 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 partant de la plus haute à la plus basse ...
  • Ensuite, il vous suffira de les renommer dans pg_enum Dans l'ordre inverse.

Illustration
Vous avez le jeu d'étiquettes suivant:

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

et vous voulez obtenir:

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

ensuite:

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

ensuite:

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

Etc...

8
benja

Mettre à jour pg_enum fonctionne, de même que l'astuce de la colonne intermédiaire mise en avant ci-dessus. On peut aussi utiliser USING magic pour changer le type de colonne directement:

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 nécessitant explicitement ou retournant cette énumération, vous êtes bon. (pgsql se plaindra quand vous laisserez tomber le type s'il y en a.)

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

5
Denis de Bernardy

Je n'arrive pas à poster un commentaire, je dirai simplement que la mise à jour de pg_enum fonctionne dans Postgres 8.4. Pour la configuration de nos énumérations, j'ai ajouté de nouvelles valeurs aux types d’énum 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 c'est logique compte tenu de la manière dont Postgres stocke ses données.

5
Josiah

Impossible d'ajouter un commentaire à l'endroit approprié, mais ALTER TABLE foo ALTER COLUMN bar TYPE new_enum_type USING bar::text::new_enum_type avec 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é.

3
Judy Morgan Loomis

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

2
user80168

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

Au lieu d'un ENUM, un DOMAIN doit être utilisé sur le type TEXT avec une contrainte vérifiant que la valeur se trouve dans la liste spécifiée de valeurs autorisées (comme suggéré par certains commentaires) . Le seul problème est qu'aucune contrainte ne peut être ajoutée (et donc non modifiée) à un domaine si celui-ci est utilisé par un type composite (la documentation indique simplement que "devrait éventuellement être amélioré"). Une telle restriction peut toutefois ê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'avais utilisé une solution similaire à la réponse acceptée, mais elle est loin d'être satisfaisante une fois que des vues ou fonctions ou des types composites (et en particulier des vues utilisant d'autres vues utilisant les ENUM modifiés ...) sont pris en compte. 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_check Aboutit malheureusement à la même erreur que l'ajout d'une nouvelle contrainte au domaine utilisé par un type composite.)

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

1
Ondřej Bouda

Voici une solution plus générale, mais plutôt rapide, qui, mis à part le fait de changer le type lui-même, met à jour toutes les colonnes de la base de données qui l'utilise. La méthode peut être appliquée même si une nouvelle version d’ENUM est différente de plus d’une étiquette ou ignore certaines des étiquettes 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 de données ne se produira. J'ai appliqué la méthode sur 5 tables en utilisant my_type et ayant entre 50 000 et 70 000 lignes dans chacune d’elles, le processus n’a pris que 10 secondes.

Bien entendu, la fonction renverra une exception si les libellés manquants dans la nouvelle version d’ENUM sont utilisés quelque part dans les données, mais dans ce cas, vous devez toujours effectuer une opération préalable.

1
Alexander Kachkaev

Comme discuté ci-dessus, la commande ALTER 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 table et calculating 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 nom 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'
    )

Notez que votre nom de type est précédé d'un trait de soulignement dans la table pg_type. De plus, le nom de type doit être en minuscule dans la clause where.

Ceci peut maintenant être écrit en toute sécurité dans votre script de migration de base de données.

0
Mahesh