It could work like this:
CREATE OR REPLACE FUNCTION upsert_expense(
p_id integer
,p_amount integer
,p_payer_id integer
,p_category category_t
,p_description text
,OUT p_expenses json)
-- RETURNS json -- not needed, since we use OUT parameter
AS
$func$
BEGIN
IF p_id = 0 THEN
INSERT INTO expenses(amount, payer_id, category, description)
VALUES (p_amount, p_payer_id, p_category, p_description)
RETURNING row_to_json(expenses.*)
INTO p_expenses;
ELSE
UPDATE expenses
SET amount = p_amount
,payer_id = p_payer_id
,category = p_category
,description = p_description
WHERE id = p_id
RETURNING row_to_json(expenses.*)
INTO p_expenses;
END IF;
-- No RETURN needed since we use OUT parameter
END
$func$ LANGUAGE plpgsql;
Major points
The RETURNING
clause in SQL INSERT
or UPDATE
statements does not return from the function. They just return a value from the SQL statement inside the function. Use the INTO
clause to tell plpgsql where to write it to.
You could DECLARE
a variable foo
and use ... RETURNING INTO foo
and later RETURN foo;
. But since you just want to return the value from the function, take a shortcut and use an OUT
parameter to begin with.
The function row_to_json()
returns data type json
, so the variable (or OUT
parameter should be of matching type!
Since you are not actually returning from the function with the SQL RETURNING
clause, you need to refit the IF
logic, lest the UPDATE
would be run every time.
Concurrency
Inserting and updating are often prone to race conditions in multi-user environments. You may want to employ a data-modifying CTE and possibly locks or more.
This does mostly the same as your function, except it decides whether to update or insert automatically based on the existence of a matching id
, while your function is instructed explicitly with id = 0
:
WITH values(id, amount, payer_id, category, description) AS (
SELECT 1, 2, 3, 'some_val'::category_t, 'some text' -- your values here
)
, upd AS (
UPDATE expenses e
SET amount = v.amount
,payer_id = v.payer_id
,category = v.category
,description = v.description
FROM values v
WHERE e.id = v.id
RETURNING row_to_json(e.*)
)
, ins AS (
INSERT INTO expenses
(amount, payer_id, category, description)
SELECT amount, payer_id, category, description -- add id?
FROM values
WHERE NOT EXISTS (SELECT 1 FROM upd) -- no row found in update
RETURNING row_to_json(expenses.*)
)
SELECT * FROM upd
UNION ALL
SELECT * FROM ins; -- one row guaranteed
Minimizes the time slot for race conditions. You can wrap this into an SQL or plpgsql function, too. But there is more ... Consider these related questions: