I'd like to write a generic procedure for Postgres 12 that updates a table's columns depending on what data is supplied in a jsonb
argument. It could be done in application logic instead of course, but I'm trying to push as much code down into the db layer as possible.
This is the kind of thing I naively hoped might work:
CREATE PROCEDURE record_event(
foo_arg integer,
name_arg text,
data_arg jsonb,
occurred_at_arg timestamptz
)
AS $$
DECLARE column_name text;
BEGIN
-- This part is incidental:
INSERT INTO foo_event(foo, name, data, occurred_at)
VALUES(foo_arg, name_arg, data_arg, occurred_at_arg);
-- This is the part I'm struggling with:
FOR column_name IN (SELECT * FROM jsonb_object_keys(data_arg)) LOOP
PREPARE update_query(text, text, integer) AS
UPDATE foo SET $1 = $2 WHERE id = $3;
EXECUTE update_query(column_name, data_arg->>column_name, foo_arg);
END LOOP;
END
$$ LANGUAGE plpgsql;
But pg doesn't like $1
in that position:
psql:src/db/migrations/foo/up.sql:43: ERROR: syntax error at or near "$1"
LINE 15: UPDATE foo SET $1 = $2 WHERE id = $3;
I also noticed there is a json_populate_record()
function which seems like it should be helpful here, but I don't really understand how to apply it to my situation from the documentation.
Does what I'm trying to do make sense and is something like it possible?
Best Answer
Your case is too dynamic for
json_populate_record()
: it takes a record type which you don't know (nor have) at the time of calling.PROCEDURE
callingUPDATE
for each JSON keyProof of concept to show what did not work in your attempt.
Call:
Column names cannot be dynamic, so format the query (with
format()
for convenience) and useEXECUTE
. But values are better provided with theUSING
clause.Note the format specifier
%I
, but the parameters$1
and$2
refer to values provided by theUSING
clause (not to function parameters!).But I would not use it. Multiple
UPDATE
commands are pretty inefficient. Instead use a ...Function with a single
UPDATE
Should be much more efficient.
Call:
We could pass values with another
USING
clause. But simply concatenate the whole command for convenience.Using a
FUNCTION
as there is nothing that would require aPROCEDURE
(new since Postgres 11). Would work as procedure too, though.As always, properly quote identifiers and values to defend against possible SQL injection. I only dare to concatenate
foo_arg
directly, as aninteger
value is safe in this regard and works as unquoted numeric literal.Note that column names are treated as case-sensitive.
Related: