Как ссылаться на уникальный индекс, который использует функцию в ON CONFLICT?

Я использую postgres 9.5.3, и у меня есть таблица вроде этого:

CREATE TABLE packages (
  id   SERIAL PRIMARY KEY,
  name VARCHAR NOT NULL
);

Я определил функцию canonical_name следующим образом:

CREATE FUNCTION canonical_name(text) RETURNS text AS $$
    SELECT replace(lower($1), '-', '_')
$$ LANGUAGE SQL;

Я добавил уникальный индекс в эту таблицу, в которой используется функция:

CREATE UNIQUE INDEX index_package_name
ON packages (canonical_name(name));
CREATE INDEX
# \d+ packages
                                                  Table "public.packages"
 Column |       Type        |                       Modifiers                       | Storage  | Stats target | Description
--------+-------------------+-------------------------------------------------------+----------+--------------+-------------
 id     | integer           | not null default nextval('packages_id_seq'::regclass) | plain    |              |
 name   | character varying | not null                                              | extended |              |
Indexes:
    "packages_pkey" PRIMARY KEY, btree (id)
    "index_package_name" UNIQUE, btree (canonical_name(name::text))

И этот уникальный индекс работает, как я ожидаю; он предотвращает вставку дубликатов:

INSERT INTO packages (name) 
VALUES ('Foo-bar');

INSERT INTO packages (name) 
VALUES ('foo_bar');

ERROR:  duplicate key value violates unique constraint "index_package_name"
DETAIL:  Key (canonical_name(name::text))=(foo_bar) already exists.

Моя проблема в том, что я хочу использовать этот уникальный индекс для выполнения upsert, и я не могу понять, как мне нужно указать цель конфликта. Документация, похоже, я могу указать индексное выражение:

where conflict_target can be one of:

    ( { index_column_name | ( index_expression ) } [ COLLATE collation ] [ opclass ] [, ...] ) [ WHERE index_predicate ]
    ON CONSTRAINT constraint_name

Но все эти вещи ниже того, что я пробовал, вызывают ошибки, как показано, вместо рабочего upsert.

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

INSERT INTO packages (name)
VALUES ('foo_bar')
ON CONFLICT (canonical_name(name)) 
DO UPDATE SET name = EXCLUDED.name;

ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification

Соответствие индексному выражению как \d+ показало это:

INSERT INTO packages (name)
VALUES ('foo_bar')
ON CONFLICT (canonical_name(name::text)) 
DO UPDATE SET name = EXCLUDED.name;


ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification

Просто назовите столбец, в котором включен уникальный индекс:

INSERT INTO packages (name)
VALUES ('foo_bar')
ON CONFLICT (name) 
DO UPDATE SET name = EXCLUDED.name;

ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification

Вместо этого используйте имя индекса:

INSERT INTO packages (name)
VALUES ('foo_bar')
ON CONFLICT (index_package_name) 
DO UPDATE SET name = EXCLUDED.name;

ERROR:  column "index_package_name" does not exist
LINE 3: ON CONFLICT (index_package_name)

Итак, как я могу указать, что я хочу использовать этот индекс? Или это ошибка?

Ответ 1

К сожалению, вы не можете сделать это с помощью PostgreSQL.

Как вы выяснили, вы можете указать только выражение для уникального ограничения, а не единственное для уникального индекса. Это несколько сбивает с толку, потому что под капотом уникальное ограничение - это просто уникальный индекс (но это рассматривается как деталь реализации).

Что еще хуже для вас, вы не можете определить уникальное ограничение по уникальному индексу, содержащему выражения – Я не уверен, в чем причина, но подозреваю стандарт SQL.

Один из способов сделать это - добавить искусственный столбец, заполненный "каноническим именем" триггером, и определить ограничение на этот столбец. Я признаю, что это не приятно.

В качестве альтернативы вы можете использовать сериализуемые транзакции и определять триггер BEFORE, который проверяет ваше состояние. Но это потребует, чтобы вы справлялись с ошибками сериализации в своем коде, что является громоздким.