I have an application wherein the user enters the inputs in comma separated format from UI and i have to capture those values and insert into a database tables in the form of records.
For example the user enters ('p1,p2,p3,p4') and it will be stored in table as
ID Value
1 p1
2 p2
3 p3
4 p4
I need to implement this using associative array?
You can create a type of array of varchar2(100):
CREATE OR REPLACE TYPE t_my_list AS TABLE OF VARCHAR2(100);
Then create a function returning this type from an inputted p_list string 'valsepvalsepvalsep' (c2t for char to table):
CREATE OR REPLACE
FUNCTION c2t(p_sep in Varchar2, p_list IN VARCHAR2)
RETURN t_my_list
AS
l_string VARCHAR2(32767) := p_list || p_sep;
l_sep_index PLS_INTEGER;
l_index PLS_INTEGER := 1;
l_tab t_my_list := t_my_list();
BEGIN
LOOP
l_sep_index := INSTR(l_string, p_sep, l_index);
EXIT
WHEN l_sep_index = 0;
l_tab.EXTEND;
l_tab(l_tab.COUNT) := TRIM(SUBSTR(l_string,l_index,l_sep_index - l_index));
l_index := l_sep_index + 1;
END LOOP;
RETURN l_tab;
END c2t;
/
Here is how you call the above:
select cto_table(',', 'p1,p2,p3') from dual;
This gives you a "collection". You make a table with it with the built-in table(..) function like this:
select rownum, column_value from table(c2t(',', 'p1,p2,p3')) ;
Related
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!
I'm relatively new to pl/sql and i'm trying to make a list with records objects but i dont know how to initialize for each item of the list both fields from record item. For example : in procedure "new item" how i can initialize example(1) ? with example(1).id_std := integer and example(1).procent := integer ? Thanks!
This is how my code looks like :
set serveroutput on;
CREATE OR REPLACE PACKAGE newExercise IS
TYPE item IS RECORD(
id_std INTEGER,
procent INTEGER
);
TYPE tabel IS VARRAY(5) OF item;
PROCEDURE newItem (example tabel);
example2 tabel := tabel();
end newExercise;
/
CREATE OR REPLACE PACKAGE BODY newExercise IS
PROCEDURE newItem (example tabel) IS
BEGIN
FOR i IN 1..example.LIMIT LOOP
DBMS_OUTPUT.PUT_LINE(example(i));
end loop;
end newItem;
end newExercise;
/
Record types are for storing the results of queries. So you could do this:
declare
recs newExercise.tabel;
begin
select level, level * 0.25
bulk collect into recs
from dual
connect by level <= 5;
newExercise.newItem (recs);
end;
/
Note that VARRAY is not a suitable collection type for this purpose, because it's not always possible to predict how many rows a query will return. It's better to use
TYPE tabel IS table OF item;
When you refer to the record you usually have to specify specific fields. This populates the records with calculated values; to be able to do that I've had to changed the procedure argument from the default IN direction to IN OUT, both in the specification:
CREATE OR REPLACE PACKAGE newExercise IS
TYPE item IS RECORD(
id_std INTEGER,
procent INTEGER
);
TYPE tabel IS VARRAY(5) OF item;
PROCEDURE newItem (example IN OUT tabel);
-- ^^^^^^ make in/out to be updateable
-- example2 tabel := tabel(); -- not used
END newExercise;
/
and in the body:
CREATE OR REPLACE PACKAGE BODY newExercise IS
PROCEDURE newItem (example IN OUT tabel) IS
-- ^^^^^^ make in/out to be updateable
BEGIN
FOR i IN 1..example.LIMIT LOOP
-- extend collection to create new record
example.extend();
-- assign values to record fields
example(i).id_std := i;
example(i).procent := 100 * (1/i);
END LOOP;
END newItem;
END newExercise;
/
The LIMIT is five, from the definition, but the varray instance is initially empty (from tabel()). For population you can loop from 1 to that limit of five, but you have to extend() the collection to actually create the record in that position. Records are created with all fields set to null by default. You can then assign values to the fields of each record. (I've just made something up, obviously).
You can then test that with an anonymous block:
declare
example newExercise.tabel := newExercise.tabel();
begin
-- call procedure
newExercise.newItem(example);
-- display contents for debuggibg
FOR i IN 1..example.COUNT LOOP
DBMS_OUTPUT.PUT_LINE('Item ' || i
|| ' id_std: ' || example(i).id_std
-- ^^^^^^^ refer to field
|| ' procent: ' || example(i).procent);
-- ^^^^^^^ refer to field
END LOOP;
end;
/
Item 1 id_std: 1 procent: 100
Item 2 id_std: 2 procent: 50
Item 3 id_std: 3 procent: 33
Item 4 id_std: 4 procent: 25
Item 5 id_std: 5 procent: 20
PL/SQL procedure successfully completed.
I've put the original loop to display the contents of the array in that block, as you wouldn't generally have that as part of a procedure. You could still use LIMIT for that loop, but COUNT is safer in case the procedure doesn't fully populate it.
You can also extend once before the loop:
PROCEDURE newItem (example IN OUT tabel) IS
BEGIN
-- extend collection to create all new records
example.extend(example.LIMIT);
FOR i IN 1..example.LIMIT LOOP
example(i).id_std := i;
example(i).procent := 100 * (1/i);
END LOOP;
END newItem;
If you already know the values you want to assign - and they aren't coming from a table, in which case you'd use APC's approach - you can just assign to the last created record; this is a rather contrived example:
PROCEDURE newItem (example IN OUT tabel) IS
BEGIN
example.extend(); -- first record
example(example.LAST).id_std := 1;
example(example.LAST).procent := 7;
example.extend(); -- second record, left with null fields
example.extend(); -- third record
example(example.LAST).id_std := 3;
example(example.LAST).procent := 21;
example.extend(); -- fourth record, left with null fields
END newItem;
and the same anonymous block now gives:
Item 1 id_std: 1 procent: 7
Item 2 id_std: procent:
Item 3 id_std: 3 procent: 21
Item 4 id_std: procent:
PL/SQL procedure successfully completed.
Notice the null values, and that there is no 5th row.
Or again extend the collection once, and refer to the numbered records directly:
PROCEDURE newItem (example IN OUT tabel) IS
BEGIN
example.extend(4);
example(1).id_std := 1;
example(1).procent := 7;
example(3).id_std := 3;
example(3).procent := 21;
END newItem;
which gets the same result from the anonymous block.
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);
I want to execute some sql statements stored in a clob in the database.
I want to make use of dbms_sql.parse with a clob as input parameter.
The code I tried as a testcase on a 11.2 Oracle database:
Making the table for the inserts:
create table table1 (t1 number(8), t2 varchar2(1), t3 varchar2(1));
The statement that fails:
DECLARE
cursor makeclob is
select 'insert into table1 (t1,t2,t3) values ('||rownum||', ''X'',''I'');' stat
from dual
connect by level < 10000;
testcl clob;
opencu integer;
err integer;
BEGIN
for rec in makeclob loop
testcl := testcl || rec.stat || '\n';
end loop;
testcl := testcl || 'commit;'|| '\n';
opencu := dbms_sql.open_cursor;
dbms_sql.parse(opencu,testcl,dbms_sql.native);
err := dbms_sql.execute(opencu);
dbms_sql.close_cursor(opencu);
END;
This statement failed with the following error:
ORA-00911: invalid character.
ORA-06512: at "SYS.DBMS_SQL", line 1250
ORA-06512: at line 17
00911. 00000 - "invalid character"
*Cause: identifiers may not start with any ASCII character other than
letters and numbers. $#_ are also allowed after the first
character. Identifiers enclosed by doublequotes may contain
any character other than a doublequote. Alternative quotes
(q'#...#') cannot use spaces, tabs, or carriage returns as
delimiters. For all other contexts, consult the SQL Language
Reference Manual.
*Action:
Does anyone know what is wrong with my statement?
You should wrap your parsed statements between
BEGIN
and
END
Also use chr(13) instead of '\n'.
I ajusted your code a little bit, so take a look at this:
DECLARE
cursor makeclob is
select 'insert into table1 (t1,t2,t3) values ('||rownum||', ''X'',''I'');' stat
from dual
connect by level < 10000;
testcl clob;
opencu integer;
err integer;
BEGIN
testcl := 'BEGIN'||chr(13);
for rec in makeclob loop
testcl := testcl || rec.stat ||chr(13);
end loop;
testcl := testcl || 'commit;'||chr(13);
testcl := testcl || 'END;';
opencu := dbms_sql.open_cursor;
dbms_sql.parse(opencu,testcl,dbms_sql.native);
err := dbms_sql.execute(opencu);
dbms_sql.close_cursor(opencu);
END;
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