PostgreSQL - Dynamic Tables, insert values from Text array in a function - arrays

A client wants to store anything in seperate tables. (Long story, it's nescessary).
To do this, i've build an Postgres function to create new tables on the fly in it's own namespace.
These tables can have 2, 4, or 100 columns, just what the user wants.
No problem, this works. The used datatypes in these dynamic tables are native, e.g. text, booleans, integers, etcetera.
Now comes the problem, i've got to insert data into these tables. The point is, the users can not access the tables directly, they'll do this through a function.
For a couple of datatypes is this not a problem, but for the text datatypes it's problematic.
Here is the function till now:
-- Function: add(integer, text[])
-- DROP FUNCTION add(integer, text[]);
CREATE OR REPLACE FUNCTION add(id integer, fields text[])
RETURNS integer AS
$BODY$
DECLARE
l_line_ending text := ')';
l_fieldtype integer;
l_ito_table_name text;
l_ito_fieldnames text;
l_field ito_fields%rowtype;
l_first_loop boolean := true;
l_values_to_insert text := 'VALUES (';
l_loop_counter integer := 0;
l_query text;
BEGIN
select into l_ito_table_name ito_table_name from ito where id = target_ito_id;
l_ito_fieldnames := 'insert into ' || l_ito_table_name || '(';
FOR l_field IN SELECT * FROM ito_fields
WHERE ito_fields.ito_id = target_ito_id
order by ito_fields.id asc
LOOP
l_loop_counter := l_loop_counter +1;
l_fieldtype := l_field.fieldtype;
if not l_first_loop THEN
l_values_to_insert := (l_values_to_insert || ', ');
end if;
if l_field.fieldtype = 1 THEN
l_values_to_insert := (l_values_to_insert || '''' || (fields[l_loop_counter]) || '''' );
elsif l_field.fieldtype = 2 THEN
l_values_to_insert := quote_literal(l_values_to_insert || private.cast_to_integer(fields[l_loop_counter]));
elsif l_field.fieldtype = 3 THEN
l_values_to_insert := quote_literal(l_values_to_insert || private.cast_to_boolean(fields[l_loop_counter]));
elsif l_field.fieldtype = 4 THEN
l_values_to_insert := quote_literal(l_values_to_insert || private.cast_to_float(fields[l_loop_counter]));
else
return 103;
end if;
if l_first_loop then
l_ito_fieldnames := l_ito_fieldnames || l_field.column_name;
l_first_loop := false;
else
l_ito_fieldnames := l_ito_fieldnames || ', ' || l_field.column_name;
end if;
END LOOP;
l_ito_fieldnames := l_ito_fieldnames || l_line_ending;
l_values_to_insert := ((l_values_to_insert) || (l_line_ending));
l_query := (l_ito_fieldnames || l_values_to_insert);
EXECUTE l_query;
return 0;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
ALTER FUNCTION add(integer, text[])
OWNER TO postgres;
The table ito_fields stores all the field metadata, so the datatype, versions, descriptions are stored here.
the ito table stores all the dynamic table data.
The point in this function are the quotes. The insert function is created dynamicly, therefore i've got to add some quotes around the text fields in the insert function.
Postgres is giving errors as soon as i do that. Even with the quote_literal functions it's still a problem, because of the string concatenation (i know, security risks, but that's no problem for now).
I've tried to use quote_literal, quote_ident, even replacing the quotes (') with a replacement, until the execute function (replace(query, l_quote_rep, '''').. I really don't have a clue now how to fix this...
Thanks in advance.

Arrays, aggregates, quote_ident and quote_nullable are your friends.
This should work, and code is shorter:
CREATE OR REPLACE FUNCTION add(id integer, fields text[])
RETURNS integer AS
$BODY$
DECLARE
l_ito_table_name text;
l_query text;
l_fields text;
r_values text;
BEGIN
--get table name
SELECT INTO l_ito_table_name quote_ident(ito_table_name) FROM ito WHERE id = target_ito_id;
-- get column names
SELECT INTO l_fields
array_to_string( array_agg(quote_ident(column_name)), ',' )
FROM ito_fields
WHERE ito_fields.ito_id = target_ito_id
order by ito_fields.id asc;
-- prepare values
SELECT INTO r_values
array_to_string( array_agg(quote_nullable(u.name)), ',' )
FROM unnest(fields) u(name);
l_query := 'insert into ' || l_ito_table_name || '(' || l_fields || ') values (' || r_values || ')';
EXECUTE l_query;
return 0; -- why 0?
END;
$BODY$;

Related

Postgresql VARADIC function, need help (beginner)

I´m new in programming with plpgsql and stored functions in postgresql.
I´ve programmed one function called this:
CREATE OR REPLACE FUNCTION public.uploadbutton(VARIADIC arr character varying[])
RETURNS text
LANGUAGE plpgsql
AS $function$DECLARE
arrayi ALIAS for $1;
result text;
begin
return result;
END;
$function$
;
As you can see, the function does not have many code. The problem is that i don´t really understand how to handle Arrays in Postgresql and plpgsql.
I need to split the array every 3 delimeters until the end of array, the array can have many inputs but minimum 3.
SELECT uploadbutton('59373033336415021231','5','a','59373033335915022fff','5','b')
Here an example for calling it.
The output should look like that:
('59373033336415021231','5','a')('59373033336415021231','5','a')
Can you teach me, how to handle that ?
Kind regards guys!
EDIT:
After getting this Result :
('59373033336415021231','5','a')('59373033336415021231','5','a')
I need to put every 3 parameters, and call a second function with the first part of the result as parameters. And the second Value have to be Int. This should look like this:
edit('59373033336415021231',5,'a')
and the same for the second part of the result
edit('59373033336415021231',5,'a')
I´m trying it at the moment and want to get the id of the first parameter with my edit() function. This is what i have:
CREATE OR REPLACE FUNCTION public.upload(string_in text)
RETURNS integer
LANGUAGE plpgsql
AS $function$DECLARE
string_in ALIAS FOR $1;
id integer;
begin
id= (select id from public.buttons where fbisn =
split_part(string_in,',',1));
return val;
end;
$function$
;
string_in is everytime the for clause of the first function is completed, the result string of that. This function will be called every time.
But currently, i will get no result.
Thanks for your help
Maybe a FOR LOOP using BY 3 is what you're struggling with
CREATE OR REPLACE FUNCTION public.uploadbutton(VARIADIC arr TEXT[])
RETURNS TEXT LANGUAGE plpgsql AS $$
DECLARE result TEXT DEFAULT '';
BEGIN
FOR i IN 1..array_length($1,1) BY 3 LOOP
result := result || '(' || array_to_string($1[i:i+2],',') || ')';
END LOOP;
RETURN result;
END;
$$;
Testing
SELECT uploadbutton(VARIADIC '{"59373033336415021231","5","a","59373033335915022fff","5","b"}'::text[]);
uploadbutton
------------------------------------------------------
(59373033336415021231,5,a)(59373033335915022fff,5,b)
(1 Zeile)
If you need the quotation marks ' in every element, just change the concatenation of the result variable accordingly:
result := result || E'(\'' || array_to_string($1[i:i+2],E'\',\'') || E'\')';
Edit: see comments
CREATE OR REPLACE FUNCTION public.uploadbutton(VARIADIC arr TEXT[])
RETURNS VOID LANGUAGE plpgsql AS $$
DECLARE slice TEXT[];
BEGIN
FOR i IN 1..array_length($1,1) BY 3 LOOP
slice := $1[i:i+2];
PERFORM edit(slice[1]::TEXT,slice[2]::INT,slice[3]::TEXT);
END LOOP;
END;
$$;
My answer may look ugly, but it works. You can optimize it if you want.
CREATE OR REPLACE FUNCTION uploadbutton(arr character varying[])
RETURNS text AS $$
DECLARE
arr_data character varying[] := arr;
array_len integer;
ar_data text;
incr INTEGER := 1;
result_data TEXT;
start_scope text := '(';
end_scope text := ')';
ins_incr integer := 1;
next_val text := '';
BEGIN
array_len := (select array_length(arr_data,1));
WHILE incr <= array_len LOOP
ar_data := CONCAT('''',arr_data[incr],'''');
IF (result_data IS NULL) THEN
result_data := CONCAT(result_data,'',ar_data);
ELSE
result_data := CONCAT(result_data,',',ar_data);
END IF;
ins_incr := ins_incr + 1;
IF (ins_incr = 4) THEN
result_data := CONCAT(start_scope,result_data,end_scope);
next_val := CONCAT(next_val,result_data);
result_data := NULL;
ins_incr := 1;
END IF;
incr := incr + 1;
END LOOP;
RETURN CONCAT(next_val,'(',result_data,')');
END $$
LANGUAGE plpgsql;
Sample:
SELECT uploadbutton(array ['59373033336415021231','5','a','59373033335915022fff','5','b','59373033335915022CCC','6','C','59373033335915022KKK','77','KK'])

update multidimensional arrays in postgresql

id Weather
1 {{KS,'S'},{MO,'S'},{CA,'S'}}
I am trying to update 'S' to 'W' for all KS,MO,CA.
I am executing below query and it is throwing me an error
UPDATE table
SET Weather[][2] ='W' where id=1;
expected output
id Weather
1 {{KS,'W'},{MO,'W'},{CA,'W'}}
I think is possible using ordinary UPDATE.
Let's try to select items what we need to replace:
-- first, second and third item from array and convert to string
SELECT array_to_string(weather[1:1], ',') AS KS,
array_to_string(weather[2:2], ',') AS MO,
array_to_string(weather[3:3], ',') AS CA
FROM test_tbl;
Result: "KS,'S'","MO,'S'","CA,'S'"
Now we can select records records for update using just comparing string with array item(as string):
SELECT * FROM test_tbl
-- KS,'S'
WHERE array_to_string(weather[1:1], ',') = concat('KS,', quote_literal('S'))
-- MO,'S'
-- array_to_string(weather[2:2], ',') = concat('MO,', quote_literal('S'))
-- CA,'S'
-- array_to_string(weather[3:3], ',') = concat('CA,', quote_literal('S'))
Ok. Now we need just to merge array by parts with a new item.
UPDATE test_tbl
-- generate first item + other items
SET weather = string_to_array(concat('KS,', quote_literal('W')), ',')::varchar[] || weather[2:]
WHERE array_to_string(weather[1:1], ',') = concat('KS,', quote_literal('S'));
UPDATE test_tbl
-- first item + generate second + other items
SET weather = weather[1:1] || string_to_array(concat('MO,', quote_literal('W')), ',')::varchar[] || weather[3:]
WHERE array_to_string(weather[2:2], ',') = concat('MO,', quote_literal('S'));
UPDATE test_tbl
-- first + second items, generate third + other items
SET weather = weather[0:2] || string_to_array(concat('CA,', quote_literal('W')), ',')::varchar[] || weather[4:]
WHERE array_to_string(weather[3:3], ',') = concat('CA,', quote_literal('S'));
Result: {{KS,'W'},{MO,'W'},{CA,'W'}}. Hope this helps
I believe that normalizing your data would be the best solution, but if it is not an option for you, try this function:
CREATE OR REPLACE FUNCTION change_array(p TEXT[][]) RETURNS TEXT[][] AS $$
DECLARE row record; res TEXT[][];
DECLARE i INT :=0;
BEGIN
LOOP
EXIT WHEN i = array_length(p,1);
i:=i+1;
IF p[i:i][1:2] <# ARRAY[['KS','S']] THEN
res := res || ARRAY[['KS','W']];
ELSEIF p[i:i][1:2] <# ARRAY[['MO','S']] THEN
res := res || ARRAY[['MO','W']];
ELSEIF p[i:i][1:2] <# ARRAY[['CA','S']] THEN
res := res || ARRAY[['CA','W']];
ELSE res := res || p[i:i];
END IF;
END LOOP;
RETURN res;
END;
$$ LANGUAGE plpgsql ;
Testing with your example..
SELECT change_array(ARRAY[['KS','S'],['MO','S'],['CA','S'],['XX','S']]);
change_array
-------------------------------
{{KS,W},{MO,W},{CA,W},{XX,S}}
(1 Zeile)

RETURN cannot have a parameter in function with OUT parameters

I did the db migration from oracle to pgsql and got the code like below:
CREATE OR REPLACE FUNCTION PKG_UTIL_BD_LOGISTICS_getsignerinfo (
i_opCode T_MQ_LOGIC_TRACK_HEAD_LOG.LP_CODE%TYPE, i_remark T_MQ_LOGIC_TRACK_HEAD_LOG.REMARK%TYPE, i_acceptTime T_MQ_LOGIC_TRACK_HEAD_LOG.SIGNED_TIME%TYPE, i_signer T_MQ_LOGIC_TRACK_HEAD_LOG.SIGNER%TYPE, i_lpcode T_MQ_LOGIC_TRACK_HEAD_LOG.LP_CODE%TYPE,
o_signer OUT T_MQ_LOGIC_TRACK_HEAD_LOG.SIGNER%TYPE, o_signerTime OUT T_MQ_LOGIC_TRACK_HEAD_LOG.SIGNED_TIME%TYPE, o_status OUT T_MQ_LOGIC_TRACK_HEAD_LOG.STATUS%TYPE )
RETURNS RECORD AS $body$
DECLARE
v_signer T_MQ_LOGIC_TRACK_HEAD_LOG.SIGNER%TYPE;
v_signerTime T_MQ_LOGIC_TRACK_HEAD_LOG.SIGNED_TIME%TYPE;
v_status T_MQ_LOGIC_TRACK_HEAD_LOG.STATUS%TYPE;
BEGIN
IF i_lpcode = 'SF' THEN
IF i_opCode = '8000' THEN
IF POSITION(':back' in i_remark) > 0 THEN
v_status := '3';
ELSE
v_status := '7';
v_signerTime := i_acceptTime;
v_signer := SUBSTR(i_remark, POSITION(':' in i_remark) + 1);
END IF;
ELSIF i_opCode = '9999' THEN
v_status := '3';
ELSIF i_opCode = '80' THEN
v_status := '7';
v_signerTime := i_acceptTime;
ELSIF i_opCode = 70 THEN
v_status := i_opCode;
ELSE
v_status := '1';
END IF;
ELSE
IF i_opCode = 'signed' THEN
v_signerTime := i_acceptTime;
v_signer := i_signer;
v_status:='7';
ELSE
v_status:='1';
END IF;
END IF;
o_status := v_status;
o_signer := v_signer;
o_signerTime := v_signerTime;
RETURN 1;
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION '%', 'PKG_UTIL_BD_LOGISTICS.getSignerInfo fetch parameters' || i_remark || 'value error:' || SQLERRM;
END;
$body$
LANGUAGE PLPGSQL
SECURITY DEFINER
when i executed the code,i got the error:RETURN cannot have a parameter in function with OUT parameters.Can someone help?I am new to pgsql.
The result of function with OUT parameters is specified by values of OUT parameters and only by these values. Although syntax of OUT parameters is similar between PostgreSQL and Oracle, a implementation is maximally different.
Oracle uses reference for OUT parameters - so you can write something like:
CREATE FUNCTION foo(a int, OUT b int)
RETURN boolean IS
BEGIN
b := a;
RETURN true;
END;
This function returns boolean value and as "side" effect it modifies second parameter passed by reference.
PostgreSQL doesn't support passing parameters by reference. All parameters are passed by value only. When You use OUT parameter, then there is not passed reference, but the returned values is taken from result composite. Result composite is composed only from OUT parameters. There are no space for some any other. So code:
CREATE OR REPLACE FUNCTION foo(a INT, OUT b int)
RETURNS boolean AS $$
BEGIN
b := a;
RETURN true;
END; $$ LANGUAGE plpgsql
is invalid, because real result of foo function is scalar int value, what is in contradiction with declared boolean. RETURN true is wrong too, because result is based on OUT parameters only, and then RETURN should be without any expression.
Equivalent translation of function foo from Oracle to Postgres is:
CREATE OR REPLACE FUNCTION foo(a INT, OUT b int, OUT result boolean)
RETURNS record AS $$
BEGIN
b := a;
result := true;
RETURN;
END; $$ LANGUAGE plpgsql
Easy rule - when function has OUT variables in Postgres, then RETURN statement is used only for ending execution - not for returned value specification. This values is based by OUT parameters.
Consider the following example :
CREATE OR REPLACE FUNCTION generate_taskcode(IN classIdVar character varying,IN appIdInt integer, OUT pagecoderet character varying) RETURNS character varying AS $$
DECLARE
updateCode character varying;
BEGIN
update mytable set lastcodeused = to_char(cast(lastcodeused as INTEGER)+1, 'FM999999999999999999') where
classid = classIdVar and appid= appIdInt
RETURNING concat(pageName,lastcodeused) as pageName
into updateCode;
return updateCode;
END;
$$ LANGUAGE plpgsql
notice the "updatecode" in between begin and end. If you'll try to execute this you'll get the same error as you've mentioned as you can't return variable "** return updateCode**" when you've OUT in function parameters. So the correct def would be as follows :
CREATE OR REPLACE FUNCTION generate_taskcode(IN classIdVar character varying,IN appIdInt integer, OUT pagecoderet character varying) RETURNS character varying AS $$
DECLARE
updateCode character varying;
BEGIN
update mytable set lastcodeused = to_char(cast(lastcodeused as INTEGER)+1, 'FM999999999999999999') where
classid = classIdVar and appid= appIdInt
RETURNING concat(pageName,lastcodeused) as pageName
into updateCode;
pagecoderet = updateCode;
return;
END;
$$ LANGUAGE plpgsql

Postgresql: Array value must start with "{" or dimension information

CREATE OR REPLACE FUNCTION f_old_transactions (IN p_fromdate date, IN p_todate date, IN p_transtype varchar,IN OUT p_cancelled boolean,
OUT p_transaction_date date,
OUT p_type varchar,
OUT p_description varchar,
OUT p_amount numeric)
RETURNS SETOF record AS
$BODY$
declare lRunQuery text;
declare lTotalRec record;
declare lBranchList text[];
declare lTranstype text[];
BEGIN
select into lTranstype
dt.type
from
v_data_types dt;
lTranstype := regexp_split_to_array(p_transtype, ',');
lrunquery := 'select
it.transaction_date trandate,
dt.type,
it.description,
ita.amount,
it.cancelled
from
import_transaction it
inner join import_transaction_account ita on it.import_transaction_id=ita.import_transaction_id
where
it.transaction_date >= ' || quote_literal(p_fromdate) || '
and it.transaction_date <= ' || quote_literal(p_todate) || '
and dt.type = any(' || quote_literall(p_transtype) || ') and';
if (p_cancelled = TRUE) then
lrunquery := lrunquery || '
it.cancelled = ' || quote_literal(p_cancelled) || '';
else
lrunquery := lrunquery || '
it.cancelled = ' || quote_literal(p_cancelled) || '';
end if;
FOR lTotalrec in
execute lRunQuery
LOOP
p_transaction_date := ltotalrec.trandate;
p_type :=ltotalrec.type;
p_description :=ltotalrec.description;
p_amount :=ltotalrec.amount;
p_cancelled := ltotalrec.cancelled;
return next;
END LOOP;
return ;
end;
$BODY$
LANGUAGE plpgsql IMMUTABLE
COST 100
ROWS 1000;
ALTER FUNCTION f_old_transactions(date,date,varchar,boolean) OWNER TO "CompuLoanPostgres";
select * from f_old_transactions ('01-Jan-2010','31-Dec-2018','Receipt Cash','FALSE')
I'm getting an error that my array value must start with "{". My array I'm trying to create is from a view v_data_type the view consist of only one column with a varchar type.
Can anyone please direct me where the issue in my code is?
Thank you in advance
I don't think you've given us enough information to know for certain what's going wrong. Notably, I have no idea what the tables should look like, either in schema or content. Without that, I can't build a test case in my own DB to debug.
That said, I noticed a couple things, specifically around the lTranstype variable:
You're assigning lTranstype twice. First you SELECT INTO it, and then you immediately assign it to a value unpacked from the p_transtype argument. It's not clear to me what you want in that variable.
When constructing your query later, you include and dt.type = any(' || quote_literall(p_transtype) || '). The problem is that p_transtype is a varchar argument, and you're trying to access it like an array. I suspect you want that to read and dt.type = any(' || quote_literall(lTranstype) || '), but I could be mistaken.
I'm guessing that your type error is coming from that second problem, but it seems like you need to reassess what the different variables in this function are intended for. Good luck.

PLPGSQL how to use an array in a execute statement?

I have a plpgsql function that executes a query, but when i put an array into it it wont parse it, at least it gives me this error:
[FAIL] syntax error at or near "{"
LINE 21: AND c.category_uuid != ANY({"c0857e20-111e-11e0-ac64-0800...
CONTEXT: PL/pgSQL function "get_membercategories" line 22 at FOR over EXECUTE statement
This is the complete line:
AND c.category_uuid != ANY({"c0857e20-111e-11e0-ac64-0800200c9a66"})
This is how my function looks
CREATE OR REPLACE FUNCTION get_membercategories(in_company_uuid uuid, in_parent_uuid uuid, in_start integer, in_limit integer, in_sortby character varying, in_order character varying, in_querystring character varying, IN in_excludestring UUID[], OUT out_status integer, OUT out_status_description character varying, OUT out_value character varying[]) RETURNS record
LANGUAGE plpgsql
AS $$
DECLARE
temp_record RECORD;
altered_parent_uuid UUID;
temp_out_value VARCHAR[];
temp_iterator INTEGER := 0;
temp_parent_uuidstring VARCHAR;
temp_excludestring VARCHAR := '';
BEGIN
IF in_parent_uuid IS NOT NULL THEN
temp_parent_uuidstring := 'AND c.parent_uuid = ''' || in_parent_uuid || '''';
ELSE
temp_parent_uuidstring := 'AND c.parent_uuid IS NULL';
END IF;
IF in_excludestring IS NOT NULL THEN
temp_excludestring := 'AND c.category_uuid != ANY(' || in_excludestring || ')';
END IF;
FOR temp_record IN EXECUTE '
SELECT
c.name,
c.category_uuid,
mc.password,
(
SELECT COUNT(*)
FROM
targetgroupusers tgu
WHERE
tgu.targetgroup_uuid = mc.targetgroup_uuid
) AS receivers
FROM
membercategories AS mc
LEFT JOIN
categories AS c
ON
mc.category_uuid = c.category_uuid
WHERE
c.isdeleted IS NULL
' || temp_excludestring || '
AND
mc.company_uuid = ''' || in_company_uuid || '''
' || in_querystring || '
' || temp_parent_uuidstring || '
ORDER BY
' || in_sortby || ' ' || in_order || '
OFFSET
' || in_start || '
LIMIT
' || in_limit
LOOP
temp_out_value[temp_iterator] = ARRAY[temp_record.category_uuid::VARCHAR(36), temp_record.name::CHARACTER VARYING, temp_record.receivers::CHARACTER VARYING, CASE WHEN temp_record.password IS NOT NULL THEN '1' ELSE '0' END];
temp_iterator = temp_iterator+1;
END LOOP;
out_status := 0;
out_status_description := 'Member categories successfully retrieved';
out_value := temp_out_value;
RETURN;
END$$;
I am clueless at this point, any help is very much appreciated!
Your problem is that the the array gets expanded to an invalid string (as you can see in the error message). The string representation of an array needs to be enclosed into single quotes again and then casted to the approriate array type:
The following should work:
IF in_excludestring IS NOT NULL THEN
temp_excludestring := 'AND c.category_uuid != ANY(''' || in_excludestring::varchar||'''::UUID[])';
END IF;
Note that in_excludestring is explicitely cast to a varchar to force the string representation of an array, enclosed in single quotes to satisfy the "array literal" syntax and then cast back to an UUID array.
The resulting string will look something like this:
category_uuid != ANY ( '{c0857e20-111e-11e0-ac64-0800200c9a66,7fffda0c-11c9-11e0-967c-a300aec7eb54}'::UUID[] )

Resources