Postgres ON CONFLICT DO UPDATE doesn't trigger if other constraints fail - postgresql

Consider the following SQL:
CREATE TABLE external_item (
id SERIAL PRIMARY KEY,
external_id TEXT UNIQUE,
enabled BOOLEAN NOT NULL CHECK (enabled = false OR external_id IS NOT NULL)
);
INSERT INTO external_item (id, enabled, external_id)
VALUES (1, true, 'ext_id_1');
INSERT INTO external_item (id, enabled)
VALUES (1, true)
ON CONFLICT (id)
DO UPDATE
SET enabled = excluded.enabled;
--> ERROR: new row for relation "external_item" violates check constraint "external_item_check"
--> DETAIL: Failing row contains (1, null, t).
The query fails because it the insert doesn't pass the check constraint. Everything works fine if you omit the CHECK from the table definition. Is there some way to set constraint priority or something so that Postgres would resort to the ON CONFLICT DO UPDATE statement before asserting other checks?

Related

Syntax error on Upsert PostgreSql while usin an insert into with on conflict [duplicate]

I'm getting the following error when doing the following type of insert:
Query:
INSERT INTO accounts (type, person_id) VALUES ('PersonAccount', 1) ON
CONFLICT (type, person_id) WHERE type = 'PersonAccount' DO UPDATE SET
updated_at = EXCLUDED.updated_at RETURNING *
Error:
SQL execution failed (Reason: ERROR: there is no unique or exclusion
constraint matching the ON CONFLICT specification)
I also have an unique INDEX:
CREATE UNIQUE INDEX uniq_person_accounts ON accounts USING btree (type,
person_id) WHERE ((type)::text = 'PersonAccount'::text);
The thing is that sometimes it works, but not every time. I randomly get
that exception, which is really strange. It seems that it can't access that
INDEX or it doesn't know it exists.
Any suggestion?
I'm using PostgreSQL 9.5.5.
Example while executing the code that tries to find or create an account:
INSERT INTO accounts (type, person_id, created_at, updated_at) VALUES ('PersonAccount', 69559, '2017-02-03 12:09:27.259', '2017-02-03 12:09:27.259') ON CONFLICT (type, person_id) WHERE type = 'PersonAccount' DO UPDATE SET updated_at = EXCLUDED.updated_at RETURNING *
SQL execution failed (Reason: ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification)
In this case, I'm sure that the account does not exist. Furthermore, it never outputs the error when the person has already an account. The problem is that, in some cases, it also works if there is no account yet. The query is exactly the same.
Per the docs,
All table_name unique indexes that, without regard to order, contain exactly the
conflict_target-specified columns/expressions are inferred (chosen) as arbiter
indexes. If an index_predicate is specified, it must, as a further requirement
for inference, satisfy arbiter indexes.
The docs go on to say,
[index_predicate are u]sed to allow inference of partial unique indexes
In an understated way, the docs are saying that when using a partial index and
upserting with ON CONFLICT, the index_predicate must be specified. It is not
inferred for you. I learned this
here, and the following example demonstrates this.
CREATE TABLE test.accounts (
id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
type text,
person_id int);
CREATE UNIQUE INDEX accounts_note_idx on accounts (type, person_id) WHERE ((type)::text = 'PersonAccount'::text);
INSERT INTO test.accounts (type, person_id) VALUES ('PersonAccount', 10);
so that we have:
unutbu=# select * from test.accounts;
+----+---------------+-----------+
| id | type | person_id |
+----+---------------+-----------+
| 1 | PersonAccount | 10 |
+----+---------------+-----------+
(1 row)
Without index_predicate we get an error:
INSERT INTO test.accounts (type, person_id) VALUES ('PersonAccount', 10) ON CONFLICT (type, person_id) DO NOTHING;
-- ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification
But if instead you include the index_predicate, WHERE ((type)::text = 'PersonAccount'::text):
INSERT INTO test.accounts (type, person_id) VALUES ('PersonAccount', 10)
ON CONFLICT (type, person_id)
WHERE ((type)::text = 'PersonAccount'::text) DO NOTHING;
then there is no error and DO NOTHING is honored.
A simple solution of this error
First of all let's see the cause of error with a simple example. Here is the table mapping products to categories.
create table if not exists product_categories (
product_id uuid references products(product_id) not null,
category_id uuid references categories(category_id) not null,
whitelist boolean default false
);
If we use this query:
INSERT INTO product_categories (product_id, category_id, whitelist)
VALUES ('123...', '456...', TRUE)
ON CONFLICT (product_id, category_id)
DO UPDATE SET whitelist=EXCLUDED.whitelist;
This will give you error No unique or exclusion constraint matching the ON CONFLICT because there is no unique constraint on product_id and category_id. There could be multiple rows having the same combination of product and category id (so there can never be a conflict on them).
Solution:
Use unique constraint on both product_id and category_id like this:
create table if not exists product_categories (
product_id uuid references products(product_id) not null,
category_id uuid references categories(category_id) not null,
whitelist boolean default false,
primary key(product_id, category_id) -- This will solve the problem
-- unique(product_id, category_id) -- OR this if you already have a primary key
);
Now you can use ON CONFLICT (product_id, category_id) for both columns without any error.
In short: Whatever column(s) you use with on conflict, they should have unique constraint.
The easy way to fix it is by setting the conflicting column as UNIQUE
I did not have a chance to play with UPSERT, but I think you have a case from
docs:
Note that this means a non-partial unique index (a unique index
without a predicate) will be inferred (and thus used by ON CONFLICT)
if such an index satisfying every other criteria is available. If an
attempt at inference is unsuccessful, an error is raised.
I solved the same issue by creating one UNIQUE INDEX for ALL columns you want to include in the ON CONFLICT clause, not one UNIQUE INDEX for each of the columns.
CREATE TABLE table_name (
element_id UUID NOT NULL DEFAULT gen_random_uuid(),
timestamp TIMESTAMP NOT NULL DEFAULT now():::TIMESTAMP,
col1 UUID NOT NULL,
col2 STRING NOT NULL ,
col3 STRING NOT NULL ,
CONSTRAINT "primary" PRIMARY KEY (element_id ASC),
UNIQUE (col1 asc, col2 asc, col3 asc)
);
Which will allow to query like
INSERT INTO table_name (timestamp, col1, col2, col3) VALUES ('timestamp', 'uuid', 'string', 'string')
ON CONFLICT (col1, col2, col3)
DO UPDATE timestamp = EXCLUDED.timestamp, col1 = EXCLUDED.col1, col2 = excluded.col2, col3 = col3.excluded;

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

I am creating a table like this:
CREATE TABLE artist (
Id serial PRIMARY KEY,
NameNormalized varchar(256) NOT NULL UNIQUE,
Name text NOT NULL,
MusicBrainzId char(36) NULL,
Rating DECIMAL(2,1),
CONSTRAINT non_empty CHECK (length(NameNormalized) > 0 and length(Name) > 0)
);
When I try to execute the following INSERT operation, I get an error:
INSERT INTO artist (NameNormalized, Name, MusicBrainzId, Rating)
VALUES ('test', 'Test', '123456789012345678901234567890123456', 0.5)
ON CONFLICT (NameNormalized) DO NOTHING;
The error message is as follows:
ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification
I have done some research but to my knowledge, setting NameNormalized to UNIQUE should be enough for ON CONFLICT to work. Also I am pretty sure this has worked in the past for me.

sql drop primary key from temp table

I want to create e temp table using select into syntax. Like:
select top 0 * into #AffectedRecord from MyTable
Mytable has a primary key. When I insert record using merge into syntax primary key be a problem. How could I drop pk constraint from temp table
The "SELECT TOP (0) INTO.." trick is clever but my recommendation is to script out the table yourself for reasons just like this. SELECT INTO when you're actually bringing in data, on the other hand, is often faster than creating the table and doing the insert. Especially on 2014+ systems.
The existence of a primary key has nothing to do with your problem. Key Constraints and indexes don't get created when using SELECT INTO from another table, the data type and NULLability does. Consider the following code and note my comments:
USE tempdb -- a good place for testing on non-prod servers.
GO
IF OBJECT_ID('dbo.t1') IS NOT NULL DROP TABLE dbo.t1;
IF OBJECT_ID('dbo.t2') IS NOT NULL DROP TABLE dbo.t2;
GO
CREATE TABLE dbo.t1
(
id int identity primary key clustered,
col1 varchar(10) NOT NULL,
col2 int NULL
);
GO
INSERT dbo.t1(col1) VALUES ('a'),('b');
SELECT TOP (0)
id, -- this create the column including the identity but NOT the primary key
CAST(id AS int) AS id2, -- this will create the column but it will be nullable. No identity
ISNULL(CAST(id AS int),0) AS id3, -- this this create the column and make it nullable. No identity.
col1,
col2
INTO dbo.t2
FROM t1;
Here's the (cleaned up for brevity) DDL for the new table I created:
-- New table
CREATE TABLE dbo.t2
(
id int IDENTITY(1,1) NOT NULL,
id2 int NULL,
id3 int NOT NULL,
col1 varchar(10) NOT NULL,
col2 int NULL
);
Notice that the primary key is gone. When I brought in id as-is it kept the identity. Casting the id column as an int (even though it already is an int) is how I got rid of the identity insert. Adding an ISNULL is how to make a column nullable.
By default, identity insert is set to off here to this query will fail:
INSERT dbo.t2 (id, id3, col1) VALUES (1, 1, 'x');
Msg 544, Level 16, State 1, Line 39
Cannot insert explicit value for identity column in table 't2' when IDENTITY_INSERT is set to OFF.
Setting identity insert on will fix the problem:
SET IDENTITY_INSERT dbo.t2 ON;
INSERT dbo.t2 (id, id3, col1) VALUES (1, 1, 'x');
But now you MUST provide a value for that column. Note the error here:
INSERT dbo.t2 (id3, col1) VALUES (1, 'x');
Msg 545, Level 16, State 1, Line 51
Explicit value must be specified for identity column in table 't2' either when IDENTITY_INSERT is set to ON
Hopefully this helps.
On a side-note: this is a good way to play around with and understand how select insert works. I used a perm table because it's easier to find.

Conditional unique constraint not updating correctly

I need to enforce uniqueness on a column but only when other column is true. For example:
create temporary table test(id serial primary key, property character varying(50), value boolean);
insert into test(property, value) values ('a', false);
insert into test(property, value) values ('a', true);
insert into test(property, value) values ('a', false);
And I enforce the uniqueness with a conditional index:
create unique index on test(property) where value = true;
So far so good, the problem arises when I try to change the row that has the value set to true. It works if I do:
update test set value = new_value from (select id, id=3 as new_value from test where property = 'a')new_test where test.id = new_test.id
But it doesn't when I do:
update test set value = new_value from (select id, id=1 as new_value from test where property = 'a')new_test where test.id = new_test.id
And I get:
ERROR: duplicate key value violates unique constraint "test_property_idx"
DETAIL: Key (property)=(a) already exists.
********** Error **********
ERROR: duplicate key value violates unique constraint "test_property_idx"
SQL state: 23505
Detail: Key (property)=(a) already exists.
Basically it works if the row with value true has a primary key with a bigger value than the current row which is truthy. Any idea on how to circumvent it?
Of course I could do:
update test set value = false where property='a';
update test set value = true where property = 'a' and id = 1;
However, I'm running these queries from node and it is preferable to run only one query.
I'm using Postgres 9.5
Your problem is that UPDATE statements cannot have an ORDER BY clause in SQL (it can have in some RDBMS, but not in PostgreSQL).
The usual solution to this is to make the constraint deferrable. But you use a partial unique index & indexes cannot be declared as deferrable.
Use an exclusion constraint instead: they are the generalization of unique constraints & can be partial too.
ALTER TABLE test
ADD EXCLUDE (property WITH =) WHERE (value = true)
DEFERRABLE INITIALLY DEFERRED;

insert several row in order to satisfy a constraint

I have two table: deck(id) and card(deck,color,value)
deck have those constraints:
CHECK (fifty_two_cards_deck(id))
PRIMARY KEY (id)
CREATE FUNCTION fifty_two_cards_deck(deck integer) RETURNS boolean
LANGUAGE sql STABLE STRICT
AS $$ SELECT COUNT(*)=52 FROM card WHERE deck=$1 $$;
and card have those constraints:
FOREIGN KEY (deck) REFERENCES deck(id)
PRIMARY KEY (deck, color, value)
How can I insert a new deck?
I tried this:
begin transaction;
INSERT INTO "public"."deck" ("id") VALUES (nextval('deck_id_seq'::regclass));
INSERT INTO "public"."card" ("deck", "color", "value") VALUES ('1', enum_first(null::Suit), enum_first(null::Symbol));
end transaction
(i had edit fifty_two_cards_deck to be a one_card_deck for testing purpose)
but I got this error:
SQL error:
ERROR: new row for relation "deck"
violates check constraint
"fifty_two_cards_deck"
In statement: begin transaction;
INSERT INTO "public"."deck" ("id")
VALUES
(nextval('deck_id_seq'::regclass));
INSERT INTO "public"."card" ("deck",
"color", "value") VALUES ('1',
enum_first(null::Suit),
enum_first(null::Symbol));
end transaction
How can I solve this without removing the constraints?
EDIT: solution
thx to Magnus Hagander I got it working like this (after setting the foreign key deferrable):
begin transaction;
SET CONSTRAINTS ALL DEFERRED;
INSERT INTO "public"."deck-card" ("deck", "position", "color", "value") VALUES (1, 0, enum_first(null::suit), enum_first(null::Symbol));
INSERT INTO "public"."deck" ("id") VALUES (1);
end transaction
It might work if you make the FOREIGN KEY with DEFERRABLE, and then set it to DEFERRED. Then you insert into the "card" table first, and then into "deck". Check constraints execute at the time of insert (thus, well before the entries in "card" exist), and cannot be deferred to transaction end.
But that's not actually going to work around the fact that your constraint is broken and should be removed ;) That CHECK constraint will only check rows going into "deck". But once the row has been inserted there, you will still be able to add more rows to, or delete rows from, the "card" table and the CHECK constraint will not complain - until the next time you try to modify "deck".