PostgreSQL C function to get values - c

I am trying to write a PostgreSQL function in C.
My goal is finding minimum value of a list. So, my function will be executed like these:
SELECT min_to_max(val) FROM (VALUES(1),(2),(3)) x(val);
SELECT min_to_max(val) FROM my_table;
Here is my C code and I lost here. For example, there is a function called "PG_GETARG_INT32" to get integer values, but I don' t know how to get values from a table in order to process. Any idea?
#include "postgres.h"
#include "fmgr.h"
PG_MODULE_MAGIC;
PG_FUNCTION_INFO_V1(get_sum);
Datum
get_sum(PG_FUNCTION_ARGS)
{
ArrayType *v1,
bool isnull;
isnull = PG_ARGISNULL(0);
if (isnull)
ereport( ERROR,
( errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("The input cannot be empty")));
List a = PG_GETARR_SOMEFUNCTION_2_GET_LIST(0);
# for loop iteration to find min_val
# return min_val
}
Edited(2022.05.13 - below is edited part):
Thanks to #Laurenz Albe. I made some progress.
Yet, now I want to go further(No needs to be in C language. As Laurenz Albe stated, I am just taking small steps).
My functions and aggregates like below to find min and max:
CREATE or replace FUNCTION find_min_func(
state integer,
next integer
) RETURNS integer
LANGUAGE plpgsql
STRICT
AS $$
declare
min_val integer;
begin
if $1 <= $2 then min_val := $1;
elsif $2 <$1 then min_val := $2;
end if;
return min_val;
END;
$$;
CREATE or replace AGGREGATE find_min(integer)
(
SFUNC = find_min_func,
STYPE = integer
);
CREATE or replace FUNCTION find_max_func(
state integer,
next integer
) RETURNS integer
LANGUAGE plpgsql
STRICT
AS $$
declare
max_val integer;
begin
if $1 >= $2 then max_val := $1;
elsif $2 > $1 then max_val := $2;
end if;
return max_val;
END;
$$;
CREATE or replace AGGREGATE find_max(integer)
(
SFUNC = find_max_func, -- State function
STYPE = integer -- State type
);
They are working great but now I want to do something like
SELECT min_to_max(val) FROM (VALUES(1),(2),(3)) x(val);
Expected output:
1 -> 3
So, I just wrote a state function and aggregate pair like below(I know it is wrong):
CREATE or replace FUNCTION find_min_and_max_func(
state integer,
next integer
) RETURNS varchar
LANGUAGE plpgsql
STRICT
AS $$
declare
min_val integer;
max_val integer;
output varchar;
begin
if $1 <= $2 then min_val := $1; max_val := $2;
elsif $2 <$1 then min_val := $2; max_val := $1;
end if;
output = cast(min_val as varchar) || '->' || cast(max_val as varchar) ;
return output;
END;
$$;
CREATE or replace AGGREGATE find_min_and_max(integer)
(
SFUNC = find_min_and_max_func, -- State function
STYPE = varchar -- State type
);
It is wrong because state function is taking arguments as integer but returns(?) varchar, so it varying.
How can I arrange my state function here?
Thanks!

To create an aggregate function, you have to use CREATE AGGREGATE. You create that in SQL. What you may need to implement in C is the state transition function (SFUNC), perhaps also others, depending on the kind of aggregate you want to create.
The aggregate function does not have to read from the table; the PostgreSQL executor will feed it the data it needs.
If you start writing C functions, you should perhaps start with something simpler than aggregate functions.

With the help of #Laurenz Albe(thanks to him again), I found the solution. Also, I checked out:
https://hoverbear.org/blog/postgresql-aggregates-with-rust/
https://hashrocket.com/blog/posts/custom-aggregates-in-postgresql
Here is my solution:
CREATE or replace FUNCTION find_min_and_max_func(
state point,
next integer
) RETURNS point
LANGUAGE plpgsql
STRICT
AS $$
declare
min_val integer;
max_val integer;
begin
if state[0] <= next then min_val := state[0];
elsif next < state[0] then min_val := next;
end if;
if state[1] >= next then max_val := state[1];
elsif next > state[1] then max_val := next;
end if;
return point(min_val, max_val) ;
END;
$$;
CREATE or replace FUNCTION find_min_and_max_final_func(
state point
) RETURNS varchar
LANGUAGE plpgsql
STRICT
AS $$
begin
return cast(state[0] as varchar) || '->' || cast(state[1] as varchar) ;
END;
$$;
CREATE or replace AGGREGATE find_min_and_max(integer)
(
SFUNC = find_min_and_max_func, -- State function
STYPE = point, -- State type
FINALFUNC = find_min_and_max_final_func,
initcond = '(1231231232131,0)'
);
SELECT find_min_and_max(value) FROM UNNEST(ARRAY [1, 2, 3]) as value;
find_min_and_max
------------------
1->6
(1 row)
Thanks!

Related

PLSQL - Convert Char Array To Varchar2

I have a function that remove last character from a varchar2, but I needed to convert it to char array first. Now I cant find anything to convert it back to varchar2.
My function:
DECLARE
TYPE CHAR_ARRAY IS TABLE OF CHAR(1) INDEX BY PLS_INTEGER;
NAME VARCHAR2(100) := '&vname';
NAME_CHAR CHAR_ARRAY;
BEGIN
FOR X IN 1..LENGTH(NAME) LOOP
IF((X = LENGTH(NAME))) THEN
NAME_CHAR(X) := '';
ELSE
NAME_CHAR(X) := SUBSTR(NAME, X , 1);
END IF;
END LOOP;
-- Now I need to convert it back to varchar2
END;
How about:
name := '';
for x in 1..name_char.count loop
name := name || name_char(x);
end loop;
Though why you would do any of this eludes me! If I wanted to remove the last character from a string I would do this:
name := substr (name, 1, length(name)-1);

How to loop through string[] in postgresql?

In this postgressql function i created a array by spliting a string. Now i want loop on this array and do some processing on it.
Function:
CREATE OR REPLACE FUNCTION getAllFoo() RETURNS character varying as
$BODY$
DECLARE
arr_split_data text[];
counter character varying;
begin
counter := ''; -- Init value
-- split data. Add in array
select into arr_split_data regexp_split_to_array('a,b,c,d,e,f',',');
FOR r IN arr_split_data -- error
LOOP
counter := arr_split_data[r] || '_' || counter; -- do some processing
END LOOP;
return counter;
END
$BODY$
LANGUAGE 'plpgsql';
But I am getting this error
when I execute this function. Is my syntax for loop is wrong?
The syntax is
FOREACH r IN ARRAY arr_split_data
LOOP
counter := r || '_' || counter;
-- do some processing
END LOOP;
You'll need to declare r too:
DECLARE
arr_split_data TEXT [];
r CHARACTER VARYING;
counter CHARACTER VARYING;
BEGIN
See section 41.6.5 of the manual: Looping Through Arrays
CREATE OR REPLACE FUNCTION getAllFoo() RETURNS character varying as
$BODY$
DECLARE
r character varying;
arr_split_data text[];
counter character varying;
begin
counter := ''; -- Init value
-- split data. Add in array
select into arr_split_data regexp_split_to_array('a,b,c,d,e,f',',');
FOREACH r IN array arr_split_data LOOP
counter := counter || '_' || r; -- do some processing
END LOOP;
return counter;
END
$BODY$
LANGUAGE 'plpgsql';

What data structure to use in order to sort this data in PL/SQL?

This is Oracle 11.2g. In a PL/SQL function, I've got a loop whereby each iteration, I create a string and an integer associated with that string. The function returns the final concatenation of all the generated strings, sorted (depending on a function input parameter), either alphabetically or by the value of the integer. To give an idea, I'm generating something like this:
Iteration String Integer
1 Oslo 40
2 Berlin 74
3 Rome 25
4 Paris 10
If the input parameter says to sort alphabetically, the function output should look like this :
Berlin, Oslo, Paris, Rome
Otherwise, we return the concatenated strings sorted by the value of the associated integer:
Paris, Rome, Oslo, Berlin
What is the most appropriate data structure to achieve this sort? I've looked at collections, associative arrays and even varrays. I've been kind of shocked how difficult this seems to be to achieve in Oracle. I saw this question but it doesn't work in my case, as I need to be able to sort by both index and value: How to sort an associative array in PL/SQL? Is there a more appropriate data structure for this scenario, and how would you sort it?
Thanks!
It is very easy if you use PL/SQL as SQL and not like other languages. It is quite specific and sometimes is very nice exactly because of that.
Sometimes I really hate PL/SQL, but this case is absolutely about love.
See how easy it is:
create type it as object (
iter number,
stringval varchar2(100),
intval integer
);
create type t_it as table of it;
declare
t t_it := new t_it();
tmp1 varchar2(32767);
tmp2 varchar2(32767);
begin
t.extend(4);
t(1) := new it(1,'Oslo',40);
t(2) := new it(2,'Berlin',74);
t(3) := new it(3,'Rome',25);
t(4) := new it(4,'Paris',10);
select listagg(stringval,', ') within group (order by stringval),
listagg(stringval,', ') within group (order by intval)
into tmp1, tmp2
from table(t);
dbms_output.put_line(tmp1);
dbms_output.put_line(tmp2);
end;
/
drop type t_it;
drop type it;
Here you can see the problem that you must create global types, and this is what I hate it for. But they say in Oracle 12 it can be done with locally defined types so I am waiting for it :)
The output is:
Berlin, Oslo, Paris, Rome
Paris, Rome, Oslo, Berlin
EDIT
As far as you do not know the amount of iterations from the beginning the only way is to do extend on each iteration (this is only example of extending):
declare
iterator pls_integer := 1;
begin
/* some type of loop*/ loop
t.extend();
-- one way to assign
t(t.last) := new it(1,'Oslo',40);
-- another way is to use some integer iterator
t(iterator) := new it(1,'Oslo',40);
iterator := iterator + 1;
end loop;
end;
I prefer the second way because it is faster (does not calculate .last on each iteration).
This is an example of pure PL/SQL implementation that is based on the idea associative array (aka map or dictionary in other domains) is an ordered collection that is sorted by a key. That is a powerful feature that I have used multiple times. For input data structure in this example I decided to use a nested table of records (aka a list of records).
In this particular case however I'd probably go for similar implementation than in simon's answer.
create or replace package so36 is
-- input data structures
type rec_t is record (
iter number,
str varchar2(20),
int number
);
type rec_list_t is table of rec_t;
function to_str(p_list in rec_list_t, p_sort in varchar2 default 'S')
return varchar2;
end;
/
show errors
create or replace package body so36 is
function to_str(p_list in rec_list_t, p_sort in varchar2 default 'S')
return varchar2 is
v_sep constant varchar2(2) := ', ';
v_ret varchar2(32767);
begin
if p_sort = 'S' then
-- create associative array (map) v_map where key is rec_t.str
-- this means the records are sorted by rec_t.str
declare
type map_t is table of rec_t index by varchar2(20);
v_map map_t;
v_key varchar2(20);
begin
-- populate the map
for i in p_list.first .. p_list.last loop
v_map(p_list(i).str) := p_list(i);
end loop;
v_key := v_map.first;
-- generate output string
while v_key is not null loop
v_ret := v_ret || v_map(v_key).str || v_sep;
v_key := v_map.next(v_key);
end loop;
end;
elsif p_sort = 'I' then
-- this branch is identical except the associative array's key is
-- rec_t.int and thus the records are sorted by rec_t.int
declare
type map_t is table of rec_t index by pls_integer;
v_map map_t;
v_key pls_integer;
begin
for i in p_list.first .. p_list.last loop
v_map(p_list(i).int) := p_list(i);
end loop;
v_key := v_map.first;
while v_key is not null loop
v_ret := v_ret || v_map(v_key).str || v_sep;
v_key := v_map.next(v_key);
end loop;
end;
end if;
return rtrim(v_ret, v_sep);
end;
end;
/
show errors
declare
v_list so36.rec_list_t := so36.rec_list_t();
v_item so36.rec_t;
begin
v_item.iter := 1;
v_item.str := 'Oslo';
v_item.int := 40;
v_list.extend(1);
v_list(v_list.last) := v_item;
v_item.iter := 2;
v_item.str := 'Berlin';
v_item.int := 74;
v_list.extend(1);
v_list(v_list.last) := v_item;
v_item.iter := 3;
v_item.str := 'Rome';
v_item.int := 25;
v_list.extend(1);
v_list(v_list.last) := v_item;
v_item.iter := 4;
v_item.str := 'Paris';
v_item.int := 10;
v_list.extend(1);
v_list(v_list.last) := v_item;
dbms_output.put_line(so36.to_str(v_list));
dbms_output.put_line(so36.to_str(v_list, 'I'));
end;
/
show errors

plpgsql function no results

I have done the following function in plpgsql. It works fine and the it returns 'OK', BUT for some reason it inputs nothing in the table temp.
CREATE OR REPLACE FUNCTION public.get_route(node integer[])
RETURNS character varying AS
$BODY$DECLARE
i int := 0;
_r record;
vertex int;
BEGIN
FOREACH i IN ARRAY node
LOOP
IF( i < (array_length(node, 1) - 1))
THEN
FOR _r IN EXECUTE 'select * from shortest_path(''
select id as id, source_id::int4 as source, target_id::int4 as target, cost, reverse_cost
from network_of_point'','|| node[i] ||','|| node[i+1] ||', true, true)'
LOOP
vertex := _r.vertex_id;
EXECUTE 'insert into temp
select nextval(''road_intersection_id_seq''), point
from distinct_network_point
where distinct_network_point.id ='|| vertex;
END LOOP;
i = i + 1;
END IF;
END LOOP;
RETURN 'OK';
END;$BODY$
The following is the synatx I am using to call the function:
select get_route('{2,7}')
It doesn't work because this line
IF( i < (array_length(node, 1) - 1))
You're testing if 2 or 7 (taken from your example) are less than the length of the array, that's valued 2.
That's because your next queries are never executed.
Because of this line, inner part of code never executed.
IF( i < (array_length(node, 1) - 1))
Actually you are extracting value of i from node array
FOREACH i IN ARRAY node
which has value 2 or 7 as in your example and you are treating i as node index which is wrong. I have created a separate int variable value, now we can use i as array index.
I think you want to achieve this.
CREATE OR REPLACE FUNCTION public.get_route(node integer[])
RETURNS character varying AS
$BODY$
DECLARE
i int := 1;
_r record;
vertex int;
value int;
BEGIN
FOREACH value IN ARRAY node
LOOP
IF( i < (array_length(node, 1)))
THEN
FOR _r IN EXECUTE 'select * from shortest_path(''
select id as id, source_id::int4 as source, target_id::int4 as target, cost, reverse_cost
from network_of_point'','|| node[i] ||','|| node[i+1] ||', true, true)'
LOOP
vertex := _r.vertex_id;
EXECUTE 'insert into temp
select nextval(''road_intersection_id_seq''), point
from distinct_network_point
where distinct_network_point.id ='|| vertex;
END LOOP;
i = i + 1;
END IF;
END LOOP;
RETURN 'OK';
END;
$BODY$ language plpgsql;

How to apply a function to each element of an array column in Postgres?

A Pg query returns an array. I would like to retrieve that with each element formatted to 3 decimal places. How can I apply a function to each element of an array? Something like the following (wrong, obviously) --
SELECT Round(ARRAY[1.53224,0.23411234], 2);
{1.532, 0.234}
I guess I am looking for something like Perl's map function.
First, turn the array into a set using unnest:
> SELECT n FROM unnest(ARRAY[1.53224,0.23411234]) AS n;
n
------------
1.53224
0.23411234
(2 rows)
Then, apply an expression to the column:
> SELECT ROUND(n, 2) FROM unnest(ARRAY[1.53224,0.23411234]) AS n;
round
-------
1.53
0.23
(2 rows)
Finally, use array_agg to turn the set back into an array:
> SELECT array_agg(ROUND(n, 2)) FROM unnest(ARRAY[1.53224,0.23411234]) AS n;
array_agg
-------------
{1.53,0.23}
(1 row)
postgres=# select array(select round(unnest(array[1.2,2.4,3,4])));
array
-----------
{1,2,3,4}
(1 row)
You may need to create a stored function. Here is the one that does what you need:
CREATE OR REPLACE FUNCTION array_round(float[], int)
RETURNS float[]
AS
$$
DECLARE
arrFloats ALIAS FOR $1;
roundParam ALIAS FOR $2;
retVal float[];
BEGIN
FOR I IN array_lower(arrFloats, 1)..array_upper(arrFloats, 1) LOOP
retVal[I] := round(CAST(arrFloats[I] as numeric), roundParam);
END LOOP;
RETURN retVal;
END;
$$
LANGUAGE plpgsql
STABLE
RETURNS NULL ON NULL INPUT;
Then call something like this:
# SELECT array_round(ARRAY[1.53224,0.23411234], 2);
array_round
-------------
{1.53,0.23}
(1 row)
You need to turn the array into a row set. For example, using generate_series:
SELECT ARRAY(SELECT ROUND(ARRAY[1.53224,0.23411234])[i], 2) FROM generate_series(1,2) AS s(i));
I know that's pretty ugly. There should be a helper function to make such mappings easier.
Perhaps something like (yes it's horrible, slow, and brittle dynamic code):
CREATE OR REPLACE FUNCTION map_with_arg(TEXT, ANYARRAY, TEXT)
RETURNS ANYARRAY
IMMUTABLE STRICT
LANGUAGE 'plpgsql' AS
$$
DECLARE
i INTEGER;
t TEXT;
cmd TEXT;
BEGIN
FOR i IN array_lower($2, 1) .. array_upper($2, 1) LOOP
cmd := 'SELECT ('||quote_ident($1)||'('||quote_nullable($2[i])||', '||quote_nullable($3)||'))::TEXT';
EXECUTE cmd INTO t;
$2[i] := t;
END LOOP;
RETURN $2;
END;
$$;
select map_with_arg('repeat', array['can','to']::TEXT[], '2');
map_with_arg
---------------
{cancan,toto}
Update It occurs to me that we could use a single dynamic statement for the whole loop. This could mitigate some of the performance concerns.
CREATE OR REPLACE FUNCTION map_with_arg(TEXT, ANYARRAY, TEXT)
RETURNS ANYARRAY
IMMUTABLE STRICT
LANGUAGE 'plpgsql' AS
$$
DECLARE
cmd TEXT;
rv TEXT;
BEGIN
cmd := 'SELECT ARRAY(SELECT (' || quote_ident($1)||'($1[i], '||quote_nullable($3)||'))::TEXT FROM generate_subscripts($1, 1) AS gs(i))';
EXECUTE cmd USING $2 INTO rv;
RETURN rv;
END;
$$;

Resources