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

Добавление нового значения в существующий тип ENUM

У меня есть столбец таблицы, который использует тип enum. Я хочу обновить этот тип enum, чтобы иметь дополнительное возможное значение. Я не хочу удалять любые существующие значения, просто добавьте новое значение. Какой самый простой способ сделать это?

4b9b3361

Ответ 1

ПРИМЕЧАНИЕ Если вы используете PostgreSQL 9.1 или более поздней версии и можете вносить изменения вне транзакции, см. этот ответ для более простого подхода.


У меня была такая же проблема несколько дней назад и я нашел этот пост. Поэтому мой ответ может быть полезен для тех, кто ищет решение :)

Если у вас есть только один или два столбца, которые используют тип перечисления, который вы хотите изменить, вы можете попробовать это. Также вы можете изменить порядок значений в новом типе.

-- 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 следует повторить, если имеется более 1 столбца.

Ответ 2

PostgreSQL 9.1 вводит возможность ALTER Типы перечислений:

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

Ответ 3

Возможное решение заключается в следующем; предварительным условием является отсутствие конфликтов в используемых значениях перечисления. (например, при удалении значения enum убедитесь, что это значение больше не используется.)

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

Кроме того, порядок столбцов не будет изменен.

Ответ 4

Если вы попадете в ситуацию, когда вы должны добавить значения enum в транзакции, f.e. выполните его в миграции flyway в ALTER TYPE, вы получите ошибку ERROR: ALTER TYPE ... ADD cannot run inside a transaction block (см. вопрос пролета # 350), вы можете добавить такие значения в pg_enum непосредственно в качестве обходного пути (type_egais_units - имя цели 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 )

Ответ 5

В дополнение к @Dariusz 1

Для Rails 4.2.1 есть этот раздел документа:

== Транзакционные миграции

Если адаптер базы данных поддерживает транзакции DDL, все миграции будут   автоматически включается в транзакцию. Есть вопросы, которые вы   не могут выполнять внутри транзакции, хотя и для этих ситуаций   вы можете отключить автоматические транзакции.

class ChangeEnum < ActiveRecord::Migration
  disable_ddl_transaction!

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

Ответ 6

Из Postgres 9.1 Документация:

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

Пример:

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

Ответ 7

Отказ от ответственности: Я не пробовал это решение, поэтому оно может не работать ;-)

Вы должны смотреть на pg_enum. Если вы хотите изменить только метку существующего ENUM, простое ОБНОВЛЕНИЕ сделает это.

Чтобы добавить новые значения ENUM:

  • Сначала вставьте новое значение в pg_enum. Если новое значение должно быть последним, все готово.
  • Если нет (вам нужно новое значение ENUM между существующими), вам придется обновить каждое отдельное значение в вашей таблице, начиная с самого верхнего до самого нижнего...
  • Тогда вам просто нужно переименовать их в pg_enum в обратном порядке.

Иллюстрация
У вас есть следующий набор меток:

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

и вы хотите получить:

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

затем:

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

затем:

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

И так далее...

Ответ 8

Я не могу опубликовать комментарий, поэтому я просто скажу, что обновление pg_enum работает в Postgres 8.4. Для того, как настроены наши перечисления, я добавил новые значения для существующих типов перечислений через:

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

Это немного страшно, но это имеет смысл, учитывая, как Postgres фактически сохраняет свои данные.

Ответ 9

Обновление pg_enum работает, как и трюк промежуточного столбца, выделенный выше. Также можно использовать ИСПОЛЬЗОВАНИЕ магии для прямого изменения типа столбца:

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;

Пока у вас нет функций, которые явно требуют или возвращают это перечисление, вы хороши. (pgsql будет жаловаться, когда вы отбрасываете тип, если есть.)

Также обратите внимание, что PG9.1 представляет инструкцию ALTER TYPE, которая будет работать с перечислениями:

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

Ответ 10

Невозможно добавить комментарий в соответствующее место, но ALTER TABLE foo ALTER COLUMN bar TYPE new_enum_type USING bar::text::new_enum_type с по умолчанию в столбце не удалось. Мне пришлось:

ALTER table ALTER COLUMN bar DROP DEFAULT;

а затем он работал.

Ответ 11

Простейший: избавиться от перечислений. Они не являются легко модифицируемыми и, следовательно, редко используются очень.

Ответ 12

Вот более общее, но довольно быстродействующее решение, которое, кроме изменения самого типа, обновляет все столбцы в базе данных, используя его. Метод может применяться, даже если новая версия ENUM отличается более чем одной меткой или пропускает некоторые из исходных. Код ниже заменяет my_schema.my_type AS ENUM ('a', 'b', 'c') на 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();

Весь процесс будет выполняться довольно быстро, потому что, если порядок ярлыков сохраняется, фактическое изменение данных не произойдет. Я применил метод на 5 таблицах с использованием my_type и имел 50 000-70 000 строк в каждом, и весь процесс занял всего 10 секунд.

Конечно, функция вернет исключение в случае, если метки, отсутствующие в новой версии ENUM, используются где-то в данных, но в такой ситуации что-то должно быть сделано заранее в любом случае.

Ответ 13

Для тех, кто ищет решение в транзакции, похоже, что работает.

Вместо ENUM a DOMAIN следует использовать в типе TEXT с ограничением, проверяющим, что значение находится в указанном списке допустимых значений (как предложено некоторыми комментариями). Единственная проблема заключается в том, что никакое ограничение не может быть добавлено (и, следовательно, не изменено) в домен, если оно используется любым составным типом (документы просто говорят, что это "должно быть в конечном итоге улучшено" ). Однако такое ограничение может быть выполнено, используя ограничение, вызывающее функцию, следующим образом.

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;

Раньше я использовал решение, аналогичное принятому ответу, но оно далека от хорошего, когда рассматриваются представления или функции или составные типы (и особенно представления с использованием других представлений с использованием модифицированных ENUM...). Решение, предложенное в этом ответе, похоже, работает при любых условиях.

Единственный недостаток заключается в том, что никакие проверки не выполняются над существующими данными при удалении некоторых допустимых значений (что может быть приемлемым, особенно для этого вопроса). (Вызов ALTER DOMAIN test_domain VALIDATE CONSTRAINT val_check заканчивается той же ошибкой, что и к добавлению нового ограничения в домен, используемый сложным типом, к сожалению.)

Обратите внимание, что небольшая модификация, такая как CHECK (value = ANY(get_allowed_values())), где функция get_allowed_values() вернула список допустимых значений, не будет работать - что довольно странно, поэтому я надеюсь, что предлагаемое выше решение работает надежно (оно для меня, до сих пор...). (это работает, на самом деле - это была моя ошибка)

Ответ 14

Как обсуждалось выше, команда ALTER не может быть записана внутри транзакции. Предлагаемый способ - вставить в таблицу pg_enum напрямую, с помощью retrieving the typelem from pg_type table и calculating the next enumsortorder number;

Ниже приведен код, который я использую. (Проверяет, существует ли дублирующее значение перед вставкой (ограничение между именем enumtypid и именем 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'
    )

Обратите внимание, что вашему имени типа предшествует знак подчеркивания в таблице pg_type. Кроме того, имя типа должно быть строчными в предложении where.

Теперь это можно безопасно записать в сценарий переноса БД.

Ответ 15

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

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;

Ответ 16

При использовании Navicat вы можете перейти к типам (в режиме просмотра → другие → типы) - получить проектный вид этого типа - и нажать кнопку "добавить метку".