Given this type:
-- Just for testing purposes:
CREATE TYPE testType as (name text)
I can get the value of a field dynamically with this function:
CREATE OR REPLACE FUNCTION get_field(object anyelement, field text) RETURNS text as
$BODY$
DECLARE
value text;
BEGIN
EXECUTE 'SELECT $1."' || field || '"'
USING object
INTO value;
return value;
END;
$BODY$
LANGUAGE plpgsql
Calling get_field('(david)'::testType, 'name')
works as expected returning "david".
But how can I set a value of a field in a composite type? I've tried these functions:
CREATE OR REPLACE FUNCTION set_field_try1(object anyelement, field text, value text)
RETURNS anyelement
as
$BODY$
DECLARE
value text;
BEGIN
EXECUTE '$1."' || field || '" := $2'
USING object, value;
return object;
END;
$BODY$
LANGUAGE plpgsql
CREATE OR REPLACE FUNCTION set_field_try2(object anyelement, field text, value text)
RETURNS anyelement
as
$BODY$
DECLARE
value text;
BEGIN
EXECUTE 'SELECT $1 INTO $2."' || field || '"'
USING value, object;
return object;
END;
$BODY$
LANGUAGE plpgsql
CREATE OR REPLACE FUNCTION set_field_try3(object anyelement, field text, value text)
RETURNS anyelement
as
$BODY$
DECLARE
value text;
BEGIN
EXECUTE 'BEGIN $1."' || field || '" := $2; SELECT $1; END;'
INTO object
USING value, object;
return object;
END;
$BODY$
LANGUAGE plpgsql
and some variations.
Calling set_field_tryX
doesn't work. I always get "ERROR: syntax error at or near...".
How can I accomplish this?
Notes:
anyelement
and the field can be any field in the composite type. I can't just use object.name.hstore
Since Postgres 9.0, with the additional module hstore
installed in your database there is a very simple and fast solution with the #=
operator that ...
replace[s] fields in
record
with matching values fromhstore
.
To install the module:
CREATE EXTENSION hstore;
Examples:
SELECT my_record #= '"field"=>"value"'::hstore; -- with string literal
SELECT my_record #= hstore(field, value); -- with values
Values have to be cast to text
and back, obviously.
Example plpgsql functions with more details:
json
There are similar, but currently undocumented (as of pg 9.5) solutions with json
(pg 9.3+) or jsonb
(pg 9.4+), built into the Postgres, so you don't need an additional module.
See @Geir's added answer for details.
hstore
and json
If you are on an older version or cannot install the additional module hstore
or cannot assume it's installed, here is an improved version of what I posted previously. Still slower than the hstore
operator, though:
CREATE OR REPLACE FUNCTION f_setfield(INOUT _comp_val anyelement
, _field text, _val text)
RETURNS anyelement AS
$func$
BEGIN
EXECUTE 'SELECT ' || array_to_string(ARRAY(
SELECT CASE WHEN attname = _field
THEN '$2'
ELSE '($1).' || quote_ident(attname)
END AS fld
FROM pg_catalog.pg_attribute
WHERE attrelid = pg_typeof(_comp_val)::text::regclass
AND attnum > 0
AND attisdropped = FALSE
ORDER BY attnum
), ',')
USING _comp_val, _val
INTO _comp_val;
END
$func$ LANGUAGE plpgsql STABLE;
Call:
CREATE TEMP TABLE t( a int, b text); -- Composite type for testing
SELECT f_setfield(NULL::t, 'a', '1');
An explicit cast of the value _val
to the target data type is not necessary, a string literal in the dynamic query would be coerced automatically, obviating the subquery on pg_type
. But I took it one step further:
Replace quote_literal(_val)
with direct value insertion via the USING
clause. Saves one function call and two casts, and is safer anyway. text
is coerced to the target type automatically in modern PostgreSQL. (Did not test with versions before 9.1.)
array_to_string(ARRAY())
is faster than string_agg()
.
No variables needed, no DECLARE
. Fewer assignments.
No subquery in the dynamic SQL. ($1).field
is faster.
pg_typeof(_comp_val)::text::regclass
does the same as
(SELECT typrelid FROM pg_catalog.pg_type WHERE oid = pg_typeof($1)::oid)
for valid composite types, just faster.
This last modification is built on the assumption that pg_type.typname
is always identical to the associated pg_class.relname
for registered composite types, and the double cast can replace the subquery. I ran this test in a big database to verify, and it came up empty as expected:
SELECT *
FROM pg_catalog.pg_type t
JOIN pg_namespace n ON n.oid = t.typnamespace
WHERE t.typrelid > 0 -- exclude non-composite types
AND t.typrelid IS DISTINCT FROM
(quote_ident(n.nspname ) || '.' || quote_ident(typname))::regclass
The use of an INOUT
parameter obviates the need for an explicit RETURN
. This is just a notational shortcut. Pavel won't like it, he prefers an explicit RETURN
statement ...
Everything put together this is almost twice as fast as the previous version.
The result is a version that's ~ 2.25 times faster. But I probably couldn't have done it without building on Pavel's second version.
In addition, this version avoids most of the casting to text and back by doing everything within a single query, so it should be much less error prone.
Tested with PostgreSQL 9.0 and 9.1.
CREATE FUNCTION f_setfield(_comp_val anyelement, _field text, _val text)
RETURNS anyelement AS
$func$
DECLARE
_list text;
BEGIN
_list := (
SELECT string_agg(x.fld, ',')
FROM (
SELECT CASE WHEN a.attname = $2
THEN quote_literal($3) || '::'|| (SELECT quote_ident(typname)
FROM pg_catalog.pg_type
WHERE oid = a.atttypid)
ELSE quote_ident(a.attname)
END AS fld
FROM pg_catalog.pg_attribute a
WHERE a.attrelid = (SELECT typrelid
FROM pg_catalog.pg_type
WHERE oid = pg_typeof($1)::oid)
AND a.attnum > 0
AND a.attisdropped = false
ORDER BY a.attnum
) x
);
EXECUTE 'SELECT ' || _list || ' FROM (SELECT $1.*) x'
USING $1
INTO $1;
RETURN $1;
END
$func$ LANGUAGE plpgsql STABLE;