How to loop through a delimited list in Oracle PLSQL - oracle

I am working on an Oracle procedure that calls another procedure within it. One of my parameters (parm1) can contain one or more values in a comma separated list. How can I loop through these values to pass them one at a time to another procedure?
Here is an example of what I would like it to do:
When Parm1 = 123,312
callProcedure2(123)
callProcedure2(321)
-or-
When Parm1 123
callProcedure2(123)
I think this can be accomplished using a loop but I can't figure out how to get it to use each value as a separated call within the loop.
Any help would be appreciated!
Thanks!

CURSOR V_CUR IS
select regexp_substr(Parm1 ,'[^,]+', 1, level) As str from dual
connect by regexp_substr(Parm1, '[^,]+', 1, level) is not null;
This curor will give you result like this
123
321
Now iterate the cursor and call the procedure in loop.
For i IN V_CUR
LOOP
callProdcedure2(i.str);
END LOOP;

Just loop through substrings:
declare
parm1 varchar2(1000) := '123,234,345,456,567,789,890';
vStartIdx binary_integer;
vEndIdx binary_integer;
vCurValue varchar2(1000);
begin
vStartIdx := 0;
vEndIdx := instr(parm1, ',');
while(vEndIdx > 0) loop
vCurValue := substr(parm1, vStartIdx+1, vEndIdx - vStartIdx - 1);
-- call proc here
dbms_output.put_line('->'||vCurValue||'<-');
vStartIdx := vEndIdx;
vEndIdx := instr(parm1, ',', vStartIdx + 1);
end loop;
-- Call proc here for last part (or in case of single element)
vCurValue := substr(parm1, vStartIdx+1);
dbms_output.put_line('->'||vCurValue||'<-');
end;

There is a utility procedure COMMA_TO_TABLE and array type DBMS_UTILITY.UNCL_ARRAY dedicated for this task. Since Oracle 10g.
It is well document here.
Here is a sample solution:
SET SERVEROUTPUT ON
DECLARE
csvListElm VARCHAR2(4000) := 'elm1, elm2,elm3 ,elm4 , elm5';
csvListTable DBMS_UTILITY.UNCL_ARRAY;
csvListLen BINARY_INTEGER;
currTableName VARCHAR2(222);
BEGIN
DBMS_UTILITY.COMMA_TO_TABLE(csvListElm, csvListLen, csvListTable);
FOR csvElm IN 1..(csvListTable.COUNT - 1) LOOP
dbms_output.put_line('-- CSV element : <'||csvListTable(csvElm)||'>');
dbms_output.put_line('-- Trimmed CSV element: <'||trim(csvListTable(csvElm))||'>');
END LOOP;
END;
/
Sample output:
-- CSV element : <elm1>;
-- Trimmed CSV element: <elm1>;
-- CSV element : < elm2>;
-- Trimmed CSV element: <elm2>;
-- CSV element : <elm3 >;
-- Trimmed CSV element: <elm3>;
-- CSV element : <elm4 >;
-- Trimmed CSV element: <elm4>;
-- CSV element : < elm5>;
-- Trimmed CSV element: <elm5>;

It is possible to use a function that you can use in a for loop (without regexp for ThinkJet):
Create a type and function
CREATE OR REPLACE TYPE t_my_list AS TABLE OF VARCHAR2(100);
CREATE OR REPLACE
FUNCTION cto_table(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 cto_table;
/
Then how to call it in the for loop:
DECLARE
parm1 varchar2(4000) := '123,234,345,456,567,789,890';
BEGIN
FOR x IN (select * from (table(cto_table(',', parm1)) ) )
LOOP
dbms_output.put_line('callProdcedure2 called with ' || x.COLUMN_VALUE);
callProdcedure2(x.COLUMN_VALUE);
END LOOP;
END;
/
Notice the default name COLUMN_VALUE given by Oracle, which is necessary for the use I want to make of the result.
Result as expected:
callProdcedure2 called with 123
callProdcedure2 called with 234
...

Related

How to modify a CLOB line containing a certain value by using a trigger with PLSQL?

I am struggling with an issue regarding a CLOB.
I would like to create a trigger (after update) which updates the column of a table which is a CLOB.
This CLOB contains lines in this form :
foo|132|65|12/08/2016|18395|
bar|132|54|15/08/2014|32434343|
I would like to modify the CLOB such that the line beginning with "foo" has the value "18395" divided by 1000. The line will look like
foo|132|65|12/08/2016|18.395|
Is there a quick way to modify my CLOB?
Thank you for your help.
EDIT : I found a way to modify the line of the CLOB what I need to do is just to modify the CLOB to replace
foo|132|65|12/08/2016|18395|
by
foo|132|65|12/08/2016|18.395|
This is rather long but effective.
Firstly create a type:
create or replace TYPE "array_str" AS VARRAY(10) OF VARCHAR(256);
Then create a function that will return a array based in a delimiter:
FUNCTION string_to_array (
string_delimited IN VARCHAR2,
delimiter IN VARCHAR2 DEFAULT ','
) RETURN array_str IS
pls_idx PLS_INTEGER;
split_string array_str := array_str();
pls_del_len PLS_INTEGER := length(delimiter);
BEGIN
IF string_delimited IS NOT NULL THEN
LOOP
-- search for delimiter string
pls_idx := instr(l_string_delimited, delimiter);
-- increase the size of array
split_string.extend;
-- check last search of delimiter is success
IF pls_idx = 0 THEN
split_string(l_split_string.count) := substr(l_string_delimited, 1);
ELSE
split_string(l_split_string.count) := substr(l_string_delimited, 1, pls_idx - 1);
END IF;
-- exit from loop when last string
EXIT WHEN nvl(l_pls_idx, 0) = 0;
string_delimited := substr(l_string_delimited, pls_idx + pls_del_len);
END LOOP;
END IF;
RETURN split_string;
END string_to_array;
Then create your trigger:
CREATE OR REPLACE TRIGGER your_trigger AFTER
UPDATE ON table_name
FOR EACH ROW
DECLARE
clob_array array_str;
clob_str CLOB := '';
BEGIN
clob_array := string_to_array(:new.new_clob, '|');
IF ( clob_array(1) = 'foo' ) THEN
clob_array(5) := to_number(clob_array(5) / 1000);
END IF;
FOR i IN 1..clob_array.count - 1 LOOP clob_str := clob_str
|| to_char(clob_array(i))
|| '|';
END LOOP;
-- do something with clob_str
END;

Wrong number or TYPES of arguments, error in PL/SQL

I have to create a list of RECORD and I need to send it to a procedure.
There is my header.
CREATE OR REPLACE PACKAGE tema4 IS
TYPE obj IS RECORD(id INTEGER := 0,percent INTEGER := 0);
TYPE listObj IS TABLE OF obj INDEX BY PLS_INTEGER;
PROCEDURE ex1 (p_listObj IN listObj);
END tema4;
My body.
create or replace PACKAGE BODY tema4 IS
PROCEDURE ex1 (p_listObj IN listObj) IS
BEGIN
DBMS_OUTPUT.PUT_LINE('Cant reach');
END ex1;
END tema4;
And my code that calls procedure ex1.
DECLARE
TYPE obj IS RECORD(id INTEGER := 0,percent INTEGER := 0);
TYPE listObj IS TABLE OF obj INDEX BY PLS_INTEGER;
v_obj obj;
v_listObj listObj;
BEGIN
FOR v_i IN (SELECT ID,BURSA FROM STUDENTI ORDER BY ID) LOOP
v_obj.id := v_i.id;
v_obj.percent := 50;
v_listObj(v_i.id) := v_obj;
END LOOP;
FOR v_i IN v_listObj.FIRST..v_listObj.LAST LOOP
DBMS_OUTPUT.PUT_LINE(v_listObj(v_i).id || ' - ' ||
v_listObj(v_i).percent);
END LOOP;
tema4.ex1(v_listObj); --this line is with problems
END;
PLS-00306: wrong number or types of arguments in call to 'EX1'
Can someone explain me what is wrong in my code? I also tried to create my type as global, but it won't let me because of 'RECORD' keyword.
Don't declare the types again (new types), use the types already declared in the package spec:
DECLARE
v_obj tema4.obj;
v_listObj tema4.listObj;
BEGIN
FOR v_i IN (SELECT ID,BURSA FROM STUDENTI ORDER BY ID) LOOP
v_obj.id := v_i.id;
v_obj.percent := 50;
v_listObj(v_i.id) := v_obj;
END LOOP;
FOR v_i IN v_listObj.FIRST..v_listObj.LAST LOOP
DBMS_OUTPUT.PUT_LINE(v_listObj(v_i).id || ' - ' ||
v_listObj(v_i).percent);
END LOOP;
tema4.ex1(v_listObj); --this line is with problems
END;

How to populate nested object table in pl/sql block?

I struggle a problem, which, i think, is rather simple.
I have a type T_OPERATION_TAG in a database which is created as:
CREATE OR REPLACE TYPE t_operation_tag AS OBJECT(
tag_name VARCHAR2(30),
tag_value VARCHAR2(30),
CONSTRUCTOR FUNCTION t_operation_tag RETURN SELF AS RESULT
)
I also have another type T_OPERATION_TAGS, which is defined as follows
CREATE OR REPLACE TYPE t_operation_tags AS TABLE OF t_operation_tag;
Then in my pl/sql block i have the following code
DECLARE
p_op_tags t_operation_tags;
BEGIN
p_op_tags := t_operation_tags();
FOR i IN (SELECT tag_name, tag_value
FROM op_tags_table
WHERE some_condition)
LOOP
--How to append new lines to p_op_tags ?
END LOOP;
END;
So, if the SELECT-query in the FOR LOOP returns,e.g., five lines then how can I populate my P_OP_TAGS object table with these five lines?
Like this:
DECLARE
p_op_tags t_operation_tags;
p_cursor sys_refcursor;
p_limit number := 5;
BEGIN
open p_cursor for
SELECT t_operation_tag(tag_name, tag_value)
FROM op_tags_table
;
fetch p_cursor bulk collect into p_op_tags limit p_limit;
DBMS_OUTPUT.put_line(p_op_tags(4).tag_name);
close p_cursor;
END;
Or if you prefer the loop clause:
DECLARE
p_op_tag t_operation_tag;
p_op_tags t_operation_tags;
p_limit number := 5;
BEGIN
p_op_tags := t_operation_tags();
for i in (SELECT tag_name, tag_value
FROM op_tags_table
WHERE some_condition
and rownum < p_limit + 1)
loop
p_op_tag := t_operation_tag(i.tag_name, i.tag_value);
p_op_tags.extend();
p_op_tags(p_op_tags.COUNT) := p_op_tag;
end loop;
DBMS_OUTPUT.put_line(p_op_tags(4).tag_name);
END;
/
You don't really need a cursor or loop at all, if you're populating the collection entirely from your query; you can bulk collect straight into it:
DECLARE
p_op_tags t_operation_tags;
BEGIN
SELECT t_operation_tag(tag_name, tag_value)
BULK COLLECT INTO p_op_tags
FROM op_tags_table
WHERE some_condition;
...
END;
/

Return N columns from a table function

I need to implement a table function, which I will submit a request with an unknown number of columns. It looks like:
SELECT * from TABLE (function())
where function, for example'SELECT x, y FROM z. I don't know how do this, so I'd like to hear some sort of way to solve, just as an idea.
I think what you are asking is you are getting multiple rows in the o/p when you are using
the function in select statement .
if i create a function as follows:
create or replace function get1job
(id in varchar2)
return varchar2 is
tittle jobs.JOB_TITLE%type;
begin
select job_title into tittle from jobs where job_id=id;
return tittle;
end get1job;
and use it in select statement .
i will write :
select get_job('AD_PRES') from dual;
i will get only one row
if i write :
select get_job('AD_PRES') from jobs;
the number of rows displayed will be equal to the number of rows in the table jobs.
Here is an example for a fully dynamic SQL, you can insert any SELECT statement and it prints out a corresponding HTML:
CREATE OR REPLACE PROCEDURE HtmlTable(sqlStr IN VARCHAR2) IS
cur INTEGER := DBMS_SQL.OPEN_CURSOR;
columnCount INTEGER;
describeColumns DBMS_SQL.DESC_TAB;
res INTEGER;
c INTEGER;
aCell VARCHAR2(4000);
BEGIN
DBMS_OUTPUT.PUT_LINE('<table>');
DBMS_SQL.PARSE(cur, sqlStr, DBMS_SQL.NATIVE);
DBMS_SQL.DESCRIBE_COLUMNS(cur, columnCount, describeColumns);
DBMS_OUTPUT.PUT_LINE('<thead><tr>');
FOR i IN 1..columnCount LOOP
DBMS_OUTPUT.PUT_LINE(' <td>'||describeColumns(i).COL_NAME||'</td>');
DBMS_SQL.DEFINE_COLUMN(cur, i, aCell, 4000);
END LOOP;
DBMS_OUTPUT.PUT_LINE('</tr></thead>');
res := DBMS_SQL.EXECUTE(cur);
DBMS_OUTPUT.PUT_LINE('<tbody>');
WHILE (DBMS_SQL.FETCH_ROWS(cur) > 0) LOOP
DBMS_OUTPUT.PUT_LINE('<tr>');
c := 1;
WHILE (c <= columnCount) LOOP
DBMS_SQL.COLUMN_VALUE(cur, c, aCell);
DBMS_OUTPUT.PUT_LINE(' <td>'||aCell||'</td>');
c := c + 1;
END LOOP;
DBMS_OUTPUT.PUT_LINE('</tr>');
END LOOP;
DBMS_OUTPUT.PUT_LINE('</tbody>');
DBMS_OUTPUT.PUT_LINE('</table>');
DBMS_SQL.CLOSE_CURSOR(cur);
END HtmlTable;
Use this as a base for your application. Then you can execute it like this:
BEGIN
HtmlTable('SELECT x, y FROM z');
END;

array or list into Oracle using cfprocparam

I have a list of values I want to insert into a table via a stored procedure.
I figured I would pass an array to oracle and loop through the array but I don't see how to pass an array into Oracle. I'd pass a list but I don't see how to work with the list to turn it into an array using PL/SQL (I'm fairly new to PL/SQL). Am I approaching this the wrong way?
Using Oracle 9i and CF8.
EDIT
Perhaps I'm thinking about this the wrong way? I'm sure I'm not doing anything new here...
I figured I'd convert the list to an associative array then loop the array because Oracle doesn't seem to work well with lists (in my limited observation).
I'm trying to add a product, then add records for the management team.
-- product table
productName = 'foo'
productDescription = 'bar'
...
...
etc
-- The managementteam table just has the id of the product and id of the users selected from a drop down.
The user IDs are passed in via a list like "1,3,6,20"
How should I go about adding the records to the management team table?
Here is where I am code wise
In theory I pass a list "1,2,3,4" to inserts.addProduct.
inserts.addProduct should call tools.listToArray and return an array.
inserts.addProduct recreates a list with a * delim as a test.
CREATE OR REPLACE PACKAGE tools AS
TYPE array_type is TABLE OF VARCHAR2(225) INDEX BY BINARY_INTEGER;
FUNCTION listToArray(in_list IN VARCHAR,
in_delim IN VARCHAR2 DEFAULT ',')
RETURN array_type;
END tools;
CREATE OR REPLACE PACKAGE BODY tools
AS
FUNCTION listToArray(in_list IN VARCHAR,
in_delim IN VARCHAR2 DEFAULT ',')
RETURN array_type
IS
l_token_count BINARY_INTEGER := 0;
-- l_token_tbl type_array;
i pls_integer;
l_start_pos INTEGER := 1;
l_end_pos INTEGER :=1;
p_parsed_table array_type;
BEGIN -- original work by John Spencer
WHILE l_end_pos <> 0 LOOP
l_end_pos := instr(in_list,in_delim,l_start_pos);
IF l_end_pos <> 0 THEN
l_token_count := l_token_count + 1;
p_parsed_table(l_token_count ) :=
substr(in_list,l_start_pos,l_end_pos - l_start_pos);
l_start_pos := l_end_pos + 1;
END IF;
END LOOP;
IF l_token_count = 0 THEN /* We haven't parsed anything so */
l_token_count := 1;
p_parsed_table(l_token_count) := in_list;
ELSE /* We need to get the last token */
l_token_count := l_token_count + 1;
p_parsed_table(l_token_count) := substr(in_list,l_start_pos);
END If;
RETURN p_parsed_table;
END listToArray; -- Procedure
END tools;
CREATE OR REPLACE PACKAGE inserts AS
TYPE array_type is TABLE OF VARCHAR2(225) INDEX BY BINARY_INTEGER;
PROCEDURE addProduct (inList IN VARCHAR2,
outList OUT VARCHAR2
);
END inserts;
CREATE OR REPLACE PACKAGE BODY inserts
AS
PROCEDURE addProduct (inList IN VARCHAR2,
outList OUT VARCHAR2
)
IS
i NUMBER;
localArray array_type := tools.listToArray(inList);
BEGIN
outList := '';
FOR i IN localArray.first .. localArray.last LOOP
outList := outList || '*' ||localArray(i); -- return a string just to test this mess
END LOOP;
END addProduct;
END inserts;
I'm currently getting an error "PLS-00382: expression is of wrong type" on localArray array_type := tools.listToArray(inList);
final working code (thanks so much!)
-- create sql type collection
CREATE OR REPLACE TYPE array_type is TABLE OF VARCHAR2(225);
/
CREATE OR REPLACE PACKAGE tools AS
FUNCTION listToArray(in_list IN VARCHAR,
in_delim IN VARCHAR2 DEFAULT ',')
RETURN array_type;
END tools;
/
CREATE OR REPLACE PACKAGE BODY tools
AS
FUNCTION listToArray(in_list IN VARCHAR,
in_delim IN VARCHAR2 DEFAULT ',')
RETURN array_type
IS
l_token_count BINARY_INTEGER := 0;
i pls_integer;
l_start_pos INTEGER := 1;
l_end_pos INTEGER :=1;
p_parsed_table array_type := array_type();
BEGIN
WHILE l_end_pos <> 0 LOOP
l_end_pos := instr(in_list,in_delim,l_start_pos);
IF l_end_pos <> 0 THEN
p_parsed_table.extend(1);
l_token_count := l_token_count + 1;
p_parsed_table(l_token_count ) :=
substr(in_list,l_start_pos,l_end_pos - l_start_pos);
l_start_pos := l_end_pos + 1;
END IF;
END LOOP;
p_parsed_table.extend(1);
IF l_token_count = 0 THEN /* We haven't parsed anything so */
l_token_count := 1;
p_parsed_table(l_token_count) := in_list;
ELSE /* We need to get the last token */
l_token_count := l_token_count + 1;
p_parsed_table(l_token_count) := substr(in_list,l_start_pos);
END If;
RETURN p_parsed_table;
END listToArray; -- Procedure
END tools;
/
CREATE OR REPLACE PACKAGE inserts AS
PROCEDURE addProduct (inList IN VARCHAR2,
outList OUT VARCHAR2
);
END inserts;
/
CREATE OR REPLACE PACKAGE BODY inserts
AS
PROCEDURE addProduct (inList IN VARCHAR2,
outList OUT VARCHAR2
)
IS
i NUMBER;
mylist VARCHAR(100);
localArray array_type := array_type();
BEGIN
localArray := tools.listToArray(inList);
mylist := '';
FOR i IN localArray.first .. localArray.last LOOP
mylist := mylist || localArray(i) || '*';
END LOOP;
aList := mylist;
END addProduct;
END inserts;
/
PL/SQL has supported arrays since Oracle 8.0. They used to be called PL/SQL tables which confused the heck out of everybody, so now they are called collections. Find out more.
The problem is, that they are implemented as User-Defined Types (i.e. objects). My reading of the ColdFusion documents suggests that cfprocparam only supports the "primitive" datatypes (number, varchar2, etc). So UDTs are not supported.
I'm not sure what you mean by this:
I'd pass a list but I don't see how to
work with the list to turn it into an
array using PL/SQL
If you mean you want to pass a string of comma separated values ....
"Fox in socks, Mr Knox, Sam-I-Am, The Lorax"
then I have a workaround for you. Oracle doesn't provide a built-in Tokenizer. But a long time ago John Spencer published a hand-rolled solution which works in Oracle 9i on the OTN forums. Find it here.
edit
but... Oracle hates me
Do not despair. The OTN forums have been upgraded a few times since John posted that , and the formatting seems to have corrupted the code somewhere along the way. There were a couple of compilation errors which it didn't use to have.
I have rewritten John's code, including a new function. THe main difference is that the nested table is declared as a SQL type rather than a PL/SQL type.
create or replace type tok_tbl as table of varchar2(225)
/
create or replace package parser is
function my_parse(
p_str_to_search in varchar2
, p_delimiter in varchar2 default ',')
return tok_tbl;
procedure my_parse(
p_str_to_search in varchar2
, p_delimiter in varchar2 default ','
, p_parsed_table out tok_tbl);
end parser;
/
As you can see, the function is just a wrapper to the procedure.
create or replace package body parser is
procedure my_parse ( p_str_to_search in varchar2
, p_delimiter in varchar2 default ','
, p_parsed_table out tok_tbl)
is
l_token_count binary_integer := 0;
l_token_tbl tok_tbl := tok_tbl();
i pls_integer;
l_start_pos integer := 1;
l_end_pos integer :=1;
begin
while l_end_pos != 0
loop
l_end_pos := instr(p_str_to_search,p_delimiter,l_start_pos);
if l_end_pos != 0 then
l_token_count := l_token_count + 1;
l_token_tbl.extend();
l_token_tbl(l_token_count ) :=
substr(p_str_to_search,l_start_pos,l_end_pos - l_start_pos);
l_start_pos := l_end_pos + 1;
end if;
end loop;
l_token_tbl.extend();
if l_token_count = 0 then /* we haven't parsed anything so */
l_token_count := 1;
l_token_tbl(l_token_count) := p_str_to_search;
else /* we need to get the last token */
l_token_count := l_token_count + 1;
l_token_tbl(l_token_count) := substr(p_str_to_search,l_start_pos);
end if;
p_parsed_table := l_token_tbl;
end my_parse;
function my_parse ( p_str_to_search in varchar2
, p_delimiter in varchar2 default ',')
return tok_tbl
is
rv tok_tbl;
begin
my_parse(p_str_to_search, p_delimiter, rv);
return rv;
end my_parse;
end parser;
/
The virtue of declaring the type in SQL is that we can use it in a FROM clause like this:
SQL> insert into t23
2 select trim(column_value)
3 from table(parser.my_parse('Fox in socks, Mr Knox, Sam-I-Am, The Lorax'))
4 /
4 rows created.
SQL> select * from t23
2 /
TXT
------------------------------------------------------------------------------
Fox in socks
Mr Knox
Sam-I-Am
The Lorax
SQL>

Resources