Search code examples
sqlpostgresqlrace-conditioninsert-updateselect-for-update

postgres: update a row on conflict insert and return old values


I need a query to update a row in table, but if the id doesn't exist it inserts default values. Also it has to avoid threads race conditions.

I found an answer here that should be fine https://stackoverflow.com/a/7927957/8372336

Using this query:

UPDATE tbl x
SET    tbl_id = 24
     , name = 'New Gal'
FROM  (SELECT tbl_id, name FROM tbl WHERE tbl_id = 4 FOR UPDATE) y 
WHERE  x.tbl_id = y.tbl_id
RETURNING y.tbl_id AS old_id, y.name AS old_name, x.tbl_id, x.name;

So I think it should return old values after updating and it should prevent threads race conditions.

But I need to add an insert if the row doesn't exist and also return the inserted values this time (old values doesn't make sense because they didn't exist).

So basically I need to do something like

INSERT INTO tbl 
    (...) VALUES (...) 
    RETURNING ..., ... 
ON CONFLICT DO
UPDATE tbl x
SET    tbl_id = 24
     , name = 'New Gal'
FROM  (SELECT tbl_id, name FROM tbl WHERE tbl_id = 4 FOR UPDATE) y 
WHERE  x.tbl_id = y.tbl_id
RETURNING y.tbl_id AS old_id, y.name AS old_name, x.tbl_id, x.name;

but i'm not sure if something like this could work. how can i make it work and make it sure of race conditions?


Solution

  • Somehow, this seems to work:

    insert into t (x)
        values ('a0'), ('b')
        on conflict (x) do update
            set x = excluded.x || '0'
        returning i, x, (select x from t t2 where t2.i = t.i);
    

    I am surprised because t is in scope in the subquery, but excluded is not. Hmmm . . . perhaps that is because it is not part of the on conflict clause but part of the overall insert. That starts to make sense.

    Here is a db<>fiddle for this version.

    I think your code would look like:

    INSERT INTO tbl (...)
        VALUES (...) 
    ON CONFLICT DO
    UPDATE tbl x
        SET tbl_id = 24,
            name = 'New Gal'
    RETURNING (SELECT t2.tbl_id FROM tbl t2 WHERE t2.tbl_id = tbl.tbl_id) AS old_id, 
              (SELECT t2.name FROM tbl t2 WHERE t2.tbl_id = tbl.tbl_id) AS old_name, 
              x.tbl_id, x.name;