plpgsql extract array before loop on it's elements - json

I am trying to create a plpg function taking as parameter :
[{'id_product': 100000158, 'd_price': '7,75'}, {'id_product': 100000339, 'd_price': '9,76'}]
Or maybe :
{'products': [{'id_product': 100000158, 'd_price': '7,75'}, {'id_product': 100000339, 'd_price': '9,76'}]}
Can't tell the best approach yet.
I want to transform this jsonb object or string to an array so I can loop on it.
The idea is to loop en every {'id_product': xxxxxxxxx, 'd_price': 'xxxxx'} so I if values are the same in a table.
What's the most optimal way to do this ?
I am still playing with jsonb functions.

You can create a function containing JSONB_POPULATE_RECORDSET() function
CREATE OR REPLACE FUNCTION fn_extract_elements( i_jsonb JSONB )
RETURNS TABLE (o_product VARCHAR(500), o_price VARCHAR(500))
AS $BODY$
BEGIN
RETURN QUERY
WITH tab AS
(
SELECT *
FROM JSONB_POPULATE_RECORDSET(NULL::record,i_jsonb )
AS tab(id_product VARCHAR(500), d_price VARCHAR(500))
)
SELECT *
FROM tab;
END
$BODY$
LANGUAGE plpgsql;
and invoke in such a way that
SELECT *
FROM fn_extract_elements(
'[{"id_product": "100000158", "d_price": "7,75"},
{"id_product": "100000339", "d_price": "9,76"}]'
);
o_product o_price
100000158 7,75
100000339 9,76
Demo

Here is a solution for those who might want to do something similar :
I changed the input to something like : {0: [100000158, 7.76], 1: [100000339, 9.76]}
And function :
CREATE OR REPLACE FUNCTION public.check_d_price(
p_products jsonb)
RETURNS jsonb
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
AS $BODY$
DECLARE
_key varchar;
_real_price NUMERIC;
_bad_price jsonb;
BEGIN
FOR _key IN (
SELECT jsonb_object_keys(p_products)
)
LOOP
SELECT ei.price INTO _real_price FROM product pr JOIN ecom_input ei ON (pr.ecom_id,pr.sku) = (ei.ecom_id,ei.sku_supplier) WHERE pr.id = (p_products->_key->>0)::INT;
IF _real_price <> (p_products->_key->>1)::NUMERIC THEN
_bad_price = COALESCE(_bad_price,'{}'::jsonb) || jsonb_build_object((p_products->_key->>0)::TEXT,
jsonb_build_object('d_price', p_products->_key->>1,'new_price', _real_price));
END IF;
END LOOP;
RETURN _bad_price;
END;
$BODY$;

Related

How to object in row_to_json and array_agg from multiple value

I manage to create a jsonb output, however I need to make it as object
My current code
create function extract_data()
returns jsonb
as
$$
declare
v_msgar text[];
v_result jsonb;
analyzer_test_full_desc character varying;
data_reading character varying;
begin
v_msgar := array['P|61||||^^||^^|U||||||||||||||||||||||||||<CR>',
'O|61|15^1^15|KK4002259|GLU^Glucose (GOD-POD Method)^^',
'R|170|AST^Aspartate Aminotransferase^^F|22.657989^^^^',
'R|171|ALP^Alkaline phosphatase^^F|107.636995^^^^',
'R|172|TP^Total Protein^^F|85.245151^^^^',
'R|173|TG^Triglycerides^^F|1.348633^^^^',
'R|174|HDL^HDL-Cholesterol^^F|1.238458^^^^',
'R|175|CHOL^Total Cholesterol^^F|5.073630^^^^',
'R|176|UA^Uric Acid^^F|309.705876^^^^',
'R|177|BUN^Urea^^F|4.412234^^^^'];
SELECT split_part(items[3], '^', 1)
INTO analyzer_test_full_desc
FROM (
SELECT string_to_array(element, '|') as items
FROM unnest(v_msgar) as t(element)) t
WHERE items[1] = 'R';
SELECT split_part(items[4], '^', 1)
INTO data_reading
FROM (
SELECT string_to_array(element, '|') as items
FROM unnest(v_msgar) as t(element)) t
WHERE items[1] = 'R';
SELECT array_to_json(array_agg(row_to_json(t)))
FROM (
SELECT analyzer_test_full_desc as resultId, data_reading as resultValue
) t INTO v_result;
return v_result;
end;
$$
language plpgsql;
The output is
[{"resultid": "AST", "resultvalue": "22.657989"}]
I expected the out put is something like this
[{"resultid": "AST", "resultvalue": "22.657989"},{"resultid": "ALP", "resultvalue": "107.636995"},{"resultid": "TP", "resultvalue": "85.245151"]
I try to loop the select statement however still didnt have the solution.
You do not need pl/pgsql to do this. Extract the data parts that you need in one (inner) query and then aggregate them like this:
create or replace function extract_data(text[]) returns jsonb as
$$
select jsonb_agg(to_jsonb(t.*)) from
(
select split_part(split_part(arr, '|', 3), '^', 1) as "resultId",
split_part(split_part(arr, '|', 4), '^', 1) as "resultValue"
from unnest($1) arr
where split_part(arr, '|', 1) = 'R'
) t;
$$ language sql;
select extract_data(array[
'P|61||||^^||^^|U||||||||||||||||||||||||||<CR>',
'O|61|15^1^15|KK4002259|GLU^Glucose (GOD-POD Method)^^',
'R|170|AST^Aspartate Aminotransferase^^F|22.657989^^^^',
'R|171|ALP^Alkaline phosphatase^^F|107.636995^^^^',
'R|172|TP^Total Protein^^F|85.245151^^^^',
'R|173|TG^Triglycerides^^F|1.348633^^^^',
'R|174|HDL^HDL-Cholesterol^^F|1.238458^^^^',
'R|175|CHOL^Total Cholesterol^^F|5.073630^^^^',
'R|176|UA^Uric Acid^^F|309.705876^^^^',
'R|177|BUN^Urea^^F|4.412234^^^^'
]);
In order to make the function reusable pass the data array as an argument rather than hard-code it inside the function body.
step-by-step demo:db<>fiddle
You don't necessarily need a stored procedure, a simple query is enough:
SELECT
jsonb_agg( -- 6
jsonb_build_object( -- 5
'resultid',
split_part(parts[3],'^',1), -- 4
'resultvalue',
split_part(parts[4],'^',1)
)
)
FROM mytable t,
unnest(myarray) as elem, -- 1
regexp_split_to_array(elem, '\|') as parts -- 2
WHERE parts[1] = 'R' -- 3
Expand all array elements into one record each
Split the elements into a new array on delimiter |
Filter all R elements
Take the expected parts and retrieve the value before the ^ character
Build the expected JSON elements
Aggregate them into a JSON array
However, if you need a stored procedure, you can wrap this query into one, of course:
create function extract_data()
returns jsonb
as
$$
declare
v_result jsonb;
begin
SELECT
...
INTO v_result
FROM ...
RETURN v_result;
end;
$$
language plpgsql;
Finally, you can put the array as function input parameter as well instead of taking a table input:
create function extract_data(myarray text[])
returns jsonb
as
$$
declare
v_result jsonb;
begin
SELECT
jsonb_agg(
jsonb_build_object(
'resultid',
split_part(parts[3],'^',1),
'resultvalue',
split_part(parts[4],'^',1)
)
)
INTO v_result
FROM unnest(myarray) as elem,
regexp_split_to_array(elem, '\|') as parts
WHERE parts[1] = 'R';
RETURN v_result;
end;
$$
language plpgsql;

Postgresql JSON Dynamic Expand & Increment

Hello guys ive been trying to build an software using postgresql and python.
Basically i want increment and/or dynamically expand the json
example: at first the field will be empty then:
#insert (toyota,honda,nissan)
{"toyota":1,
"honda":1,
"nissan":1}
#insert (toyota)
{"toyota":2,
"honda":1,
"nissan":1}
#insert (honda,mitsubitshi)
{"toyota":2,
"honda":2,
"nissan":1,
"mitsubitshi":1}
Yes i know it can be done by first retrieving json doing it via python but i dont it that way:
I dont have much experience with postgresql procedure or trigger feature.
Any Help will be apreciated: :-)
Normalized tables would be more performant, however json solution may be quite comfortable using this function:
create or replace function add_cars(cars jsonb, variadic car text[])
returns jsonb language plpgsql as $$
declare
new_car text;
begin
foreach new_car in array car loop
cars = cars || jsonb_build_object(new_car, coalesce(cars->>new_car, '0')::int+ 1);
end loop;
return cars;
end $$;
Find the full example in DbFiddle.
Please check function below. Hopefully it meets your requirement!
CREATE FUNCTION sp_test(json)
RETURNS VOID AS
$BODY$
DECLARE
var_sql varchar;
BEGIN
IF (EXISTS (
SELECT json_object_keys($1)
EXCEPT
SELECT column_name FROM information_schema.columns WHERE table_schema = 'your schema' AND table_name = 'test_table'
)) THEn
RAISE EXCEPTION 'There is column(s) does not exists on table'; -- Checking structure.
END IF;
var_sql := 'Update test_table t SET ' || (SELECT string_agg(CONCAT(t.key, ' = (t.', t.key, ' + ', t.value,')'),', ') FROM json_each($1) t);
EXECUTE (var_sql);
END;
$BODY$
LANGUAGE plpgsql;

PostgreSQL - RETURNING INTO array

I want to store an update's RETURNING values into a data structure so that I can use it in a subsequent query.
In this example, I'm given a list of "parent_ids", and I want to find all children whose parent is in that array. Then, I wish to update some value on them, and do other stuff.
CREATE OR REPLACE FUNCTION plpgsql_is_really_great(parent_ids bigint[])
RETURNS void AS
$$
DECLARE
found_ids bigint[];
BEGIN
UPDATE child SET
foo = bar
FROM
(SELECT id
FROM child
WHERE parent_id=ANY(parent_ids)
) as children_ids
WHERE
child.id = children_ids.id
RETURNING children_ids.id INTO found_ids; -- ???
-- do more stuff with found_ids
$$ LANGUAGE plpgsql
There are a few ways to go about this.
Say you want to call some f(id) for each id affected by the UPDATE.
In PL/pgSQL:
$$
DECLARE found_id BIGINT;
BEGIN
FOR found_id IN (UPDATE child SET foo=bar RETURNING id) LOOP
PERFORM f(found_id);
END LOOP;
END
$$
In pure SQL:
WITH updated(found_id) AS (
UPDATE child SET foo=bar RETURNING id
)
SELECT f(found_id) FROM updated;
If you want to collect all the found_ids in an array, you can simply:
$$
DECLARE array_var BIGINT[];
BEGIN
WITH updated(found_id) AS (
UPDATE child SET foo=bar RETURNING id
)
SELECT array_agg(found_id) FROM updated INTO array_var;
END
$$
Here is an example:
CREATE OR REPLACE FUNCTION exemplary ( parent_ids bigint[] )
RETURNS VOID AS $$
DECLARE
_found_ids bigint[];
BEGIN
WITH matching_children AS (
UPDATE child
SET foo = 1
WHERE parent_id = ANY ( parent_ids )
RETURNING id
)
SELECT array_agg ( id )
FROM matching_children
INTO _found_ids;
RAISE NOTICE '%', _found_ids;
RETURN;
END $$ LANGUAGE plpgsql;

PostgreSQL: ERROR: $name$ is not a scalar variable

There is a function that returns 3 parameters one of which is a composite type:
CREATE OR REPLACE FUNCTION f1(
p_text text,
OUT result_status_id smallint,
OUT result_status_msg text,
OUT result_my_type my_type
)
RETURNS record AS
$BODY$
--body here
$BODY$
LANGUAGE 'plpgsql' VOLATILE SECURITY DEFINER
COST 100
The composite type my_type looks like following:
CREATE TYPE my_type AS
(d_real real,
d_int1 integer,
d_int2 integer,
d_int3 integer,
d_int4 integer,
d_int5 integer,
d_int6 integer,
d_int7 integer,
d_int8 integer,
d_int9 integer,
d_int10 integer,
d_bool boolean,
d_date date,
d_text text
);
There is another function f2 that calls function f1 in its body:
CREATE OR REPLACE FUNCTION f2(
p_text text
)
RETURNS record AS
$BODY$
DECLARE
l_status_id smallint;
l_status_msg text;
l_my_type my_type;
BEGIN
--some logic here
--this statement fails
SELECT * FROM f1(p_text) 'x'
INTO l_status_id, l_status_msg, l_my_type;
--logic continues here
END;
$BODY$
LANGUAGE 'plpgsql' VOLATILE SECURITY DEFINER
COST 100;
The problem is that when executing sql with the function I receive the following error:
ERROR: "l_my_type" is not a scalar variable
How may one get a composite type object from another function?
You are violating the rules. The manual:
where target can be a record variable, a row variable, or a
comma-separated list of simple variables and record/row fields.
A record or row variable cannot be part of multiple-item INTO list.
One way around this:
CREATE OR REPLACE FUNCTION f2(p_text text)
RETURNS record AS
$BODY$
DECLARE
r record;
l_status_id smallint;
l_status_msg text;
l_my_type my_type;
BEGIN
SELECT *
FROM f1(p_text) x -- don't single quote 'x'
INTO r;
l_status_id := r.result_status_id;
l_status_msg := r.result_status_msg;
l_my_type := r.result_my_type;
RETURN r; -- or whatever ..
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
There are more ways. Depends where you are going with this. I hardly ever return an anonymous record.
Composite types in tuples are one of those things which is supported partially and you run into odd issues (particularly with thins like storage but that's a story for another time). One way you could accomplish this is:
CREATE TYPE return_type_for_function AS (
result_status_id smallint,
result_status_msg text,
result_my_type my_type
);
CREATE FUNCTION myfunc(....) RETURNS return_type_for_function ....
This is the way I have always done it. This is a little more mature than using OUT variables.
Here's a trivial example:
or_examples=# create table rel_examples.tabletest (id int);
CREATE TABLE
or_examples=# create table comp_table_test (test rel_examples.tabletest);
CREATE TABLE
or_examples=# create function test(int) returns comp_table_test
immutable language sql as $$
select row(row($1))::comp_table_test; $$;
CREATE FUNCTION
or_examples=# select test(1);
test
---------
("(1)")
(1 row)
However, #Chris Travers has proposed an acceptable solution, in my case it was unfortunately not possible to introduce a new type for returning data from f1 function.
Nevertheless, one may call the function f1() in the function f2() and still get the data using the following syntax:
CREATE OR REPLACE FUNCTION f2(
p_text text
)
RETURNS record AS
$BODY$
DECLARE
l_status_id smallint;
l_status_msg text;
l_my_type my_type;
l_record record;
BEGIN
--some logic here
--this statement is okay now
l_record = f1(p_text);
l_status_id = l_record.result_status_id;
l_status_msg = l_record.result_status_msg;
l_my_type = l_record.result_my_type;
--logic continues here
END;
$BODY$
LANGUAGE 'plpgsql' VOLATILE SECURITY DEFINER
COST 100;
In your f2() function, you could do the select into a record variable and then extract what you need from it, e.g.:
CREATE OR REPLACE FUNCTION f2(p_text text )
RETURNS record AS
$BODY$
DECLARE
record_var record;
BEGIN
--this statement was failing
SELECT * FROM f1(p_text) INTO record_var;
SELECT l_my_type.result_my_type.d_int1; -- this should work
END;
$BODY$
LANGUAGE 'plpgsql' VOLATILE SECURITY DEFINER
COST 100;

How can I pass an "array" of values to my stored procedure?

I want to be able to pass an "array" of values to my stored procedure, instead of calling "Add value" procedure serially.
Can anyone suggest a way to do it? am I missing something here?
Edit: I will be using PostgreSQL / MySQL, I haven't decided yet.
As Chris pointed, in PostgreSQL it's no problem - any base type (like int, text) has it's own array subtype, and you can also create custom types including composite ones. For example:
CREATE TYPE test as (
n int4,
m int4
);
Now you can easily create array of test:
select ARRAY[
row(1,2)::test,
row(3,4)::test,
row(5,6)::test
];
You can write a function that will multiply n*m for each item in array, and return sum of products:
CREATE OR REPLACE FUNCTION test_test(IN work_array test[]) RETURNS INT4 as $$
DECLARE
i INT4;
result INT4 := 0;
BEGIN
FOR i IN SELECT generate_subscripts( work_array, 1 ) LOOP
result := result + work_array[i].n * work_array[i].m;
END LOOP;
RETURN result;
END;
$$ language plpgsql;
and run it:
# SELECT test_test(
ARRAY[
row(1, 2)::test,
row(3,4)::test,
row(5,6)::test
]
);
test_test
-----------
44
(1 row)
If you plan to use MySQL 5.1, it is not possible to pass in an array.
See the MySQL 5.1 faq
If you plan to use PostgreSQL, it is possible look here
I don't know about passing an actual array into those engines (I work with sqlserver) but here's an idea for passing a delimited string and parsing it in your sproc with this function.
CREATE FUNCTION [dbo].[Split]
(
#ItemList NVARCHAR(4000),
#delimiter CHAR(1)
)
RETURNS #IDTable TABLE (Item VARCHAR(50))
AS
BEGIN
DECLARE #tempItemList NVARCHAR(4000)
SET #tempItemList = #ItemList
DECLARE #i INT
DECLARE #Item NVARCHAR(4000)
SET #tempItemList = REPLACE (#tempItemList, ' ', '')
SET #i = CHARINDEX(#delimiter, #tempItemList)
WHILE (LEN(#tempItemList) > 0)
BEGIN
IF #i = 0
SET #Item = #tempItemList
ELSE
SET #Item = LEFT(#tempItemList, #i - 1)
INSERT INTO #IDTable(Item) VALUES(#Item)
IF #i = 0
SET #tempItemList = ''
ELSE
SET #tempItemList = RIGHT(#tempItemList, LEN(#tempItemList) - #i)
SET #i = CHARINDEX(#delimiter, #tempItemList)
END
RETURN
END
You didn't indicate, but if you are referring to SQL server, here's one way.
And the MS support ref.
For PostgreSQL, you could do something like this:
CREATE OR REPLACE FUNCTION fnExplode(in_array anyarray) RETURNS SETOF ANYELEMENT AS
$$
SELECT ($1)[s] FROM generate_series(1,array_upper($1, 1)) AS s;
$$
LANGUAGE SQL IMMUTABLE;
Then, you could pass a delimited string to your stored procedure.
Say, param1 was an input param containing '1|2|3|4|5'
The statement:
SELECT CAST(fnExplode(string_to_array(param1, '|')) AS INTEGER);
results in a result set that can be joined or inserted.
Likewise, for MySQL, you could do something like this:
DELIMITER $$
CREATE PROCEDURE `spTest_Array`
(
v_id_arr TEXT
)
BEGIN
DECLARE v_cur_position INT;
DECLARE v_remainder TEXT;
DECLARE v_cur_string VARCHAR(255);
CREATE TEMPORARY TABLE tmp_test
(
id INT
) ENGINE=MEMORY;
SET v_remainder = v_id_arr;
SET v_cur_position = 1;
WHILE CHAR_LENGTH(v_remainder) > 0 AND v_cur_position > 0 DO
SET v_cur_position = INSTR(v_remainder, '|');
IF v_cur_position = 0 THEN
SET v_cur_string = v_remainder;
ELSE
SET v_cur_string = LEFT(v_remainder, v_cur_position - 1);
END IF;
IF TRIM(v_cur_string) != '' THEN
INSERT INTO tmp_test
(id)
VALUES
(v_cur_string);
END IF;
SET v_remainder = SUBSTRING(v_remainder, v_cur_position + 1);
END WHILE;
SELECT
id
FROM
tmp_test;
DROP TEMPORARY TABLE tmp_test;
END
$$
Then simply CALL spTest_Array('1|2|3|4|5') should produce the same result set as the above PostgreSQL query.
Thanks to JSON support in MySQL you now actually have the ability to pass an array to your MySQL stored procedure. Create a JSON_ARRAY and simply pass it as a JSON argument to your stored procedure.
Then in procedure, using MySQL's WHILE loop and MySQL's JSON "pathing" , access each of the elements in the JSON_ARRAY and do as you wish.
An example here https://gist.githubusercontent.com/jonathanvx/513066eea8cb5919b648b2453db47890/raw/22f33fdf64a2f292688edbc67392ba2ccf8da47c/json.sql
Incidently, here is how you would add the array to a function (stored-proc) call:
CallableStatement proc = null;
List<Integer> faultcd_array = Arrays.asList(1003, 1234, 5678);
//conn - your connection manager
conn = DriverManager.getConnection(connection string here);
proc = conn.prepareCall("{ ? = call procedureName(?) }");
proc.registerOutParameter(1, Types.OTHER);
//This sets-up the array
Integer[] dataFaults = faultcd_array.toArray(new Integer[faultcd_array.size()]);
java.sql.Array sqlFaultsArray = conn.createArrayOf("int4", dataFaults);
proc.setArray(2, sqlFaultsArray);
//:
//add code to retrieve cursor, use the data.
//: