PL/pgSQL function does not return custom (record) types as expected - postgresql

My function's job is to extract XML nodes. The code is as follows:
CREATE TYPE xml_node_looper_record AS (
allomany xml,
i integer,
actual_node text,
nodi_parts text[]
);
CREATE OR REPLACE FUNCTION ds.xml_node_looper_rec(rec xml_node_looper_record)
RETURNS SETOF xml_node_looper_record AS
$BODY$
DECLARE
nodes text[];
field_val text;
r xml_node_looper_record;
n integer;
BEGIN
nodes = xpath(rec.actual_node, rec.allomany);
IF nodes[1] IS NOT NULL THEN
rec.i = rec.i + 1;
FOR n IN 1..array_upper(nodes, 1) LOOP
IF rec.i = array_upper(rec.nodi_parts, 1) THEN
field_val = trim(ARRAY[xpath(rec.actual_node || '/text()', rec.allomany)]::text, ' {}"');
IF field_val IS NOT NULL AND field_val != '' THEN
RAISE NOTICE '% % % %', n, rec.actual_node, rec.i, field_val;
RETURN NEXT (NULL::xml, rec.i, rec.actual_node, ARRAY[field_val]::text[]);
END IF;
END IF;
SELECT ds.xml_node_looper_rec((rec.allomany, rec.i, rec.actual_node || '[' || n::text || ']' || rec.nodi_parts[rec.i + 1], rec.nodi_parts)) INTO r;
END LOOP;
END IF;
END;
$BODY$
LANGUAGE plpgsql VOLATILE COST 100;
As you can see the function is recursive and the goal is to collect field values from multiple XML nodes where we have no information, how many nodes we have under a tag. (I have a version with return table, but that method is too slow.) I use my own defined custom type, and when I check the return values with RAISE NOTICE, I can see the result in pgAdmin on the Messages tab, but the RETURN NEXT command returns nothing, only an empty table.
The parameters of my type:
allomany: an XML data
i: actual depth of nodi_parts
actual_node: XML node, I would like to extract. Can have multiple
nodes, I mark them with []. For example:
/tagone/tagtwo[]/tagthree[]/fieldname
nodi_parts: coming from actual_node, splitting it with []. For
example ARRAY["/tagone/tagtwo", "/tagthree", "/fieldname"]
What is the problem?

You don't propagate the result of nested calls. RETURN NEXT pushes a result to the stack related to the function call. But this stack is private - if caller doesn't fetch this stack, then the result is cleaned. Anyway - any function instance (the called function) has its own result stack. This stack is not shared.
The recursive table function in PL/pgSQL should to look like:
postgres=# CREATE OR REPLACE FUNCTION foo(level int) RETURNS SETOF int AS $$
BEGIN
IF level > 5 THEN RETURN; END IF;
RETURN NEXT level;
--!! must to take result of nested call
RETURN QUERY SELECT * FROM foo(level + 1);
RETURN;
END;
$$ LANGUAGE plpgsql;
postgres=# SELECT * FROM foo(1);
┌─────┐
│ foo │
╞═════╡
│ 1 │
│ 2 │
│ 3 │
│ 4 │
│ 5 │
└─────┘
(5 rows)
Your code is a equivalent of code:
postgres=# CREATE OR REPLACE FUNCTION foo(level int) RETURNS SETOF int AS $$
BEGIN
IF level > 5 THEN RETURN; END IF;
RETURN NEXT level;
-- error, only call of nested function, but returned table is lost
PERFORM foo(level + 1);
RETURN;
END;
$$ LANGUAGE plpgsql;
postgres=# SELECT * FROM foo(1);
┌─────┐
│ foo │
╞═════╡
│ 1 │
└─────┘
(1 row)

Related

postgresql manual plpgsql function declaration section not understand

https://www.postgresql.org/docs/14/plpgsql-declarations.html#PLPGSQL-DECLARATION-PARAMETERS
CREATE FUNCTION concat_selected_fields(in_t sometablename) RETURNS text AS $$
BEGIN
RETURN in_t.f1 || in_t.f3 || in_t.f5 || in_t.f7;
END;
$$ LANGUAGE plpgsql;
is in_t alias sometable alias name? I try to replicate it by following:
create function conca_selected_col_emp( in_t public.emp) returns text as $$
begin
return in_t.name || in_t.department;
end;
$$ LANGUAGE plpgsql;
Obviously, this will not work...
It should to work:
create table foo(a varchar, b varchar);
insert into foo values('hello', 'world');
create or replace function fx(arg foo)
returns varchar as $$
begin
return arg.a || ', ' || arg.b;
end;
$$ language plpgsql;
postgres=# select fx(foo) from foo;
┌──────────────┐
│ fx │
╞══════════════╡
│ hello, world │
└──────────────┘
(1 row)
or alternative syntax:
postgres=# select foo.fx from foo;
┌──────────────┐
│ fx │
╞══════════════╡
│ hello, world │
└──────────────┘
(1 row)

Postgres session variables not working inside a function

I am setting a session variable inside a postgres function and the values are not getting set.
Any help is most appreciated. Thanks in advance.
I am using "PostgreSQL 10.6, compiled by Visual C++ build 1800, 64-bit"
My code is as follows:
The function:
CREATE FUNCTION set_rp_vals(iv_rp_company varchar, iv_rp_portfolio varchar)
RETURNS integer
LANGUAGE plpgsql
SECURITY DEFINER
AS $function$
DECLARE
l_retval integer;
BEGIN
l_retval := 1;
RAISE NOTICE '1.iv_rp_company: >>> %', iv_rp_company;
RAISE NOTICE '2.iv_rp_portfolio: >>> %', iv_rp_portfolio;
--set the session variable
set rp.company = iv_rp_company;
set rp.portfolio = iv_rp_portfolio;
RETURN l_retval;
EXCEPTION
WHEN OTHERS THEN
RETURN 9;
END;
$function$
;
The function call:
SELECT set_rp_vals(iv_rp_company := 'COMPAN',iv_rp_portfolio := 'PORTOF');
--Retrieving the session variables:
select
current_setting('rp.company') as company,
current_setting('rp.portfolio') as portfolio;
The value returned by the above query:
I would use the set_config() function for this:
CREATE OR REPLACE FUNCTION set_rp_vals(iv_rp_company varchar, iv_rp_portfolio varchar)
RETURNS integer
LANGUAGE plpgsql
SECURITY DEFINER
AS $function$
DECLARE
l_retval integer;
BEGIN
l_retval := 1;
RAISE NOTICE '1.iv_rp_company: >>> %', iv_rp_company;
RAISE NOTICE '2.iv_rp_portfolio: >>> %', iv_rp_portfolio;
--set the session variable
perform set_config('rp.company', iv_rp_company, false);
perform set_config('rp.portfolio', iv_rp_portfolio, false);
RETURN l_retval;
EXCEPTION
WHEN OTHERS THEN
RETURN 9;
END;
$function$
;
CREATE FUNCTION
Execute with your values:
SELECT set_rp_vals(iv_rp_company := 'COMPAN',iv_rp_portfolio := 'PORTOF');
NOTICE: 1.iv_rp_company: >>> COMPAN
NOTICE: 2.iv_rp_portfolio: >>> PORTOF
┌─────────────┐
│ set_rp_vals │
├─────────────┤
│ 1 │
└─────────────┘
(1 row)
select
current_setting('rp.company') as company,
current_setting('rp.portfolio') as portfolio;
┌─────────┬───────────┐
│ company │ portfolio │
├─────────┼───────────┤
│ COMPAN │ PORTOF │
└─────────┴───────────┘
(1 row)

How to return multiple INSERTED ID's in Postgresql?

I have a function which takes two arguments, the first one is an integer and the second one is an array of varchars.
I want to insert just the hashes that aren't previously inserted for the campaign and then return the inserted ids — in this case, the url_hash field of the campaigns_urls table — but I keep getting the following error:
ERROR: column "hash" does not exist LINE 10: RETURNING "hash"
^
HINT: There is a column named "hash" in table "*SELECT*", but it cannot be referenced from this part of the query.
I am calling a function like this:
-- SELECT * FROM assign_urls_to_campaign(1,'{Xelgb20Lw}')
CREATE OR REPLACE FUNCTION public.assign_urls_to_campaign(
param_campaign_id integer,
param_hashes character varying(20)[]
)
RETURNS character varying(20)
LANGUAGE 'plpgsql'
VOLATILE
AS $BODY$
BEGIN
INSERT INTO campaigns_urls ("campaign_id", "url_hash")
SELECT
param_campaign_id as "id", "P"."hash"
FROM "urls" AS "U"
RIGHT OUTER JOIN (
SELECT hash FROM UNNEST(param_hashes) AS "hash"
) AS "P"
ON "U"."hash" = "P"."hash"
WHERE "U"."hash" ISNULL
RETURNING "hash";
END;
$BODY$;
There are more issues:
If functions returns more than one row, then should to use SETOF keywords after RETURNS.
PlpgSQL functions requires RETURN statement - in this case RETURN QUERY.
create table test(a int);
create or replace function foo(int)
returns setof int as $$
begin
return query
insert into test
select v from generate_series(1,$1) g(v)
returning a;
end;
$$ language plpgsql;
postgres=# select * from foo(3);
┌─────┐
│ foo │
╞═════╡
│ 1 │
│ 2 │
│ 3 │
└─────┘
(3 rows)

Update an array in a PostgreSQL table using a function

I'm trying to update a PostgreSQL table using a function.
My function:
CREATE OR REPLACE FUNCTION update_array_words(varchar, varchar[], int[], int, int)
RETURNS int AS $$
DECLARE
passed int;
j int;
k int;
BEGIN
passed := 0;
j := $4 + $5;
k := 0;
FOR i IN $4..j LOOP
UPDATE tab_files
SET key_words[i] = $2[k], num_key_words[i] = $3[k]
WHERE path_to_file = $1;
END LOOP;
RETURN passed;
END;
$$
LANGUAGE plpgsql
;
For calling my function:
SELECT update_array_words('path_name_to_file', '{"susana"}', '{1}', 1, 1);
The problem is, when I do a simple select in my PostgreSQL command line, the data from the update is null.
My select:
SELECT * FROM tab_files;
Output:
key_words num_key_words
| [0:2]={marques,NULL,NULL} | | [0:2]={3,NULL,NULL} |
What's wrong with my code?
In PostgreSQL arrays index by default starts from 1. Thus $2[k] = $2[0] (because of k := 0;) = null. Same with $3[k].
It is also not good idea to update same row in the loop several times. The better way is to select fields values into local variables, change them and then update your table once.
Update: If I guessing correctly about the purpose of the function, it could be simplified to update columns in single step without loop:
UPDATE tab_files set
key_words = key_words[1:$4-1] || array_fill($2[k],array[$5-$4+1]) || key_words[$5+1:],
num_key_words = num_key_words[1:$4-1] || array_fill($3[k],array[$5-$4+1]) || num_key_words[$5+1:]
WHERE path_to_file = $1;
You can to experiment with this using simple example:
with t(x,s,e,v) as (values(array[1,2,3,4,5,6,7,8], 2, 5, 0))
select
*,
x[1:s-1] as head,
array_fill(v, array[e-s+1]) as changed_part,
x[e+1:] as tail,
x[1:s-1] || array_fill(v, array[e-s+1]) || x[e+1:] as final_result
from t;
Result:
┌───────────────────┬───┬───┬───┬──────┬──────────────┬─────────┬───────────────────┐
│ x │ s │ e │ v │ head │ changed_part │ tail │ final_result │
├───────────────────┼───┼───┼───┼──────┼──────────────┼─────────┼───────────────────┤
│ {1,2,3,4,5,6,7,8} │ 2 │ 5 │ 0 │ {1} │ {0,0,0,0} │ {6,7,8} │ {1,0,0,0,0,6,7,8} │
└───────────────────┴───┴───┴───┴──────┴──────────────┴─────────┴───────────────────┘
However the better way is to create more general function like
create function array_replace_series(
p_array anyarray,
p_value anyelement,
p_start int,
p_end int)
returns anyarray language sql immutable
as $$
select
p_array[1:p_start-1] ||
array_fill(p_value, array[p_end-p_start+1]) ||
p_array[p_end+1:]
$$;
and then use it in your update:
UPDATE tab_files set
key_words = array_replace_series(key_words, 'susana', 1, 1),
num_key_words = array_replace_series(num_key_words, 1, 1, 1)
WHERE path_to_file = 'path_name_to_file';
And of course you will be able to reuse this function in other tasks.

Passing Dynamic number of Parameters to Function

I need to pass dynamic number of parameters to a function as well as their data types and then return a table having those parameters as fields.
Is that possible to do that in postgres?
Any idea or example is appreciated
Here is an example that could probably be improved.
Beware of SQL injection!
CREATE OR REPLACE FUNCTION create_table(
tabname text,
VARIADIC coldef text[]
) RETURNS void
LANGUAGE plpgsql STRICT AS
$$DECLARE
l integer;
i integer;
sql text;
sep text := '';
BEGIN
l := array_upper(coldef, 1);
IF l % 2 <> 0 THEN
RAISE EXCEPTION 'Number of arguments must be odd';
END IF;
sql := 'CREATE TABLE ' || quote_ident(tabname) || '(';
FOR i IN 1 .. l/2 LOOP
sql := sql || sep || quote_ident(coldef[2*i-1]) || ' ' || quote_ident(coldef[2*i]);
sep := ', ';
END LOOP;
sql := sql || ')';
EXECUTE sql;
END;$$;
It can be used like this:
test=> SELECT create_table('tabname', 'col1', 'int4', 'col2', 'text');
test=> \d tabname
Table "laurenz.tabname"
┌────────┬─────────┬───────────┐
│ Column │ Type │ Modifiers │
├────────┼─────────┼───────────┤
│ col1 │ integer │ │
│ col2 │ text │ │
└────────┴─────────┴───────────┘