27

I'm using postgres 9.5.3, and I have a table like this:

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

I have defined a function, canonical_name, like this:

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

I've added a unique index to this table that uses the function:

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

And this unique index is working as I expect; it prevents insertion of duplicates:

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.

My problem is that I want to use this unique index to do an upsert, and I can't figure out how I need to specify the conflict target. The documentation seems to say I can specify an index expression:

where conflict_target can be one of:

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

But all of these things below that I've tried produce errors as shown, instead of a working upsert.

I've tried matching the index expression as I specified it:

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

Matching the index expression as \d+ showed it:

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

Just naming the column that the unique index is on:

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

Using the index name instead:

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)

So how do I specify that I want to use this index? Or is this a bug?

carols10cents
  • 6,943
  • 7
  • 39
  • 56

1 Answers1

19

Important note: This behavior can only be observed on versions before 9.5.4. This is a bug that was fixed in 9.5.4. The rest of the answer describes the buggy behavior:

As you found out, you can only specify the expression for a unique constraint and not the one for a unique index. This is somewhat confusing because under the hood a unique constraint is just a unique index (but that is considered an implementation detail).

To make matters worse for you, you cannot define a unique constraint over a unique index that contains expressions – I am not certain what the reason is, but suspect the SQL standard.

One way you can do this would be to add an artificial column, filled with the “canonical name” by a trigger and define the constraint on that column. I admit that that is not nice.

The correct solution, however, is to upgrade to the latest minor release for PostgreSQL 9.5.

Laurenz Albe
  • 209,280
  • 17
  • 206
  • 263
  • After some experimentation, (with promoted indexes and check constraints using a function) I think the *artificial column* is the only workaround. – wildplasser Aug 14 '16 at 19:56
  • 4
    BTW, not sure about 9.5.3 but on 9.5.4 the `ON CONFLICT (canonical_name(name))` variant works fine. `PostgreSQL 9.5.4 on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2, 64-bit` – Abelisto Aug 14 '16 at 21:34
  • @Abelisto: damn: I thought I tested to test on 9.5 #head, but it appers to be 9.5.2 ... – wildplasser Aug 14 '16 at 22:32
  • 1
    Laurenz: Thank you for your detailed answer, you are correct for postgres <= 9.5.3, and your answer might be the most helpful for people unable to upgrade. @Abelisto: I upgraded to postgres 9.6beta2 (next version available in homebrew right now) and confirmed that ON CONFLICT (canonical_name(name)) worked great! If you add your comment as an answer instead, I'd be happy to accept it! – carols10cents Aug 15 '16 at 00:49
  • 2
    I don't understand, the current doc (https://www.postgresql.org/docs/current/static/sql-insert.html) clearly state that you can use numerous options for constraint_name, including index names. – Pak Dec 20 '16 at 18:00
  • @Pak Yes, but it must be an index that implements a unique constraint. You cannot use an arbitrary unique index name. – Laurenz Albe Dec 21 '16 at 16:10
  • @Laurenz Albe could you please explain how to achieve `ON CONFLICT` behaviour for unique index in postgres 11.6 `ON CONFLICT (unique_index_name)` doesn't work with error: ERROR: column "unique_index_name" does not exist `ADD CONSTRAINT unique_constraint_name UNIQUE USING INDEX unique_index_name;` doesnt work because of ERROR: index "unique_index_name" contains expressions index definirtion is:
    `CREATE UNIQUE INDEX unique_index_name ON ******(****, coalesce(f2, 0), coalesce(f3, 0));`
    – gstackoverflow Apr 07 '21 at 16:46
  • @gstackoverflow As I said, read [the documentation](https://www.postgresql.org/docs/current/sql-insert.html). – Laurenz Albe Apr 07 '21 at 18:37