Given a table with a unique constraint on a column
CREATE TABLE t
(
id int GENERATED ALWAYS AS IDENTITY,
name text NOT NULL UNIQUE
);
Let's create a single record
INSERT INTO t (name) VALUES ('a');
When I try to delete an existing record and insert a new one with the same value for the unique column in a single statement using CTE, it fails as expected by me:
WITH
deleted_cte AS (DELETE FROM t WHERE name = 'a' RETURNING id),
inserted_cte AS (INSERT INTO t (name) VALUES ('a') RETURNING id)
SELECT 1;
-- ERROR: duplicate key value violates unique constraint "t_name_key"
-- DETAIL: Key (name)=(a) already exists.
I expect DELETE and INSERT commands to run concurrently in an unspecified order and see the same snapshot of the table.
However, if I introduce dependency between the primary query and the deleting sub-statement, it works:
WITH
deleted_cte AS (DELETE FROM t WHERE name = 'a' RETURNING id),
inserted_cte AS (INSERT INTO t (name) VALUES ('a') RETURNING id)
SELECT id from deleted_cte; -- only this line modified
-- 1 <- returns id of the deleted record
SELECT id FROM t;
-- 2 <- inserted record
-- Plan of the query:
-- CTE Scan on deleted_cte
-- CTE deleted_cte
-- -> Delete on t
-- -> Seq Scan on t
-- Filter: (name = 'a'::text)
-- CTE inserted_cte
-- -> Insert on t t_1
-- -> Result
I do not understand what happens here.
Are sub-statements ordered now and INSERT is forced to run after DELETE?
Is UNIQUE check somehow moved to the end of the whole CTE?
Where in the docs can I read about this behavior or is it an unreliable thing?
I realize that this exact delete-insert can be replaced with upsert.
But in the actual code records aren't hard deleted. They instead are being soft deleted using UPDATE t SET deleted_at = now()
and then new records inserted. Unique constraint filters out soft deleted records CREATE UNIQUE INDEX ON t (name) WHERE (deleted_at IS NULL)
. So upsert wouldn't work here.