Updating table in SQLPLUS (Stored Procedure Loop with Comma Delimited Column) - oracle

Having some trouble writing my stored procedure. Using Oracle 11g
Goal: I want to be able to create separate rows in my table "info_table" from my table "places_table" with the column alternatenames. Under the column alternatenames from places_table, there is a comma delimited string with multiple alternate names. I want to create a row for each one of these alternate names in table "info_table".
ex of alternatenames column string:
Beijing,Beijingzi,Pei-ching-tzu
what I am hoping to achieve
ID Name
100000000 Beijing
100000001 Beijingzi
100000002 Pei-ching-tzu
Currently my code looks like this:
CREATE TABLE INFO_TABLE
(
INFOID NUMBER PRIMARY KEY,
NAME VARCHAR2(500),
LANGUAGE VARCHAR2(40),
STATUS VARCHAR2(50),
COUNTRY_CODE CHAR (10),
COUNTRY_CODE_2 CHAR (10),
GID CHAR(10),
SUPPLIERID CHAR(10),
LAST_MODIFIED CHAR(50)
);
CREATE SEQUENCE INFO_COUNTER
START WITH 100000000;
CREATE PROCEDURE LOAD_ALTERNATE_NAMES(ALTERNATENAMES_COLUMN VARCHAR2)
AS
COMMA_FINDER NUMBER := 1;
BEGIN
IF ALTERNATENAMES_COLUMN IS NOT NULL
THEN
<<SEPARATE_ALTERNATENAMES>> WHILE COMMA_FINDER!=0 LOOP
INSERT INTO INFO_TABLE
(INFOID, NAME, LANGUAGE, STATUS, COUNTRY_CODE, COUNTRY_CODE_2, GID, SUPPLIERID, LAST_MODIFIED)
VALUES
(INFO_COUNTER, SUBSTR(ALTERNATENAMES_COLUMN, INSTR(P.ALTERNATENAMES, ',', COMMA_FINDER+1)), NULL, 'ALTERNATE', P.COUNTRY_CODE, P.COUNTRY_CODE_2, P.GID, NULL, P.LASTMODIFIED)
FROM INFO_TABLE I, PLACES_TABLE P;
COMMA_FINDER := INSTR(ALTERNATENAMES, ',', COMMA_FINDER);
END LOOP SEPARATE_ALTERNATENAMES;
COMMA_FINDER:=1;
ENDIF;
END
/
LOAD_ALTERNATE_NAMES(SELECT ALTERNATENAMES FROM PLACES_TABLE);
currently the problem is that my INSERT statement in my loop is giving me "SQL Statement Ignored" and I am not sure why. I have taken a look at the stored procedure and loop documentation but can't figure out if I am doing something wrong or there is a typo.
can someone help me please?
Thank you in advance,
Norman

The INSERT statement has either the form:
INSERT INTO table (...) VALUES (...)
or:
INSERT INTO table (...) SELECT ... FROM ...
That's why Oracle issues an error message.
But there's more. You pass the ALTERNATENAMES string value to the stored procedure but need more data from the PLACES_TABLE. Furthermore, Oracle doesn't support stored procedure calls like this:
LOAD_ALTERNATE_NAMES(SELECT ALTERNATENAMES FROM PLACES_TABLE);
So I propose you create a stored procedure without parameters:
CREATE PROCEDURE LOAD_ALTERNATE_NAMES
AS
COMMA_FINDER NUMBER;
BEGIN
FOR REC IN (
SELECT * FROM PLACES_TABLE WHERE ALTERNATENAMES IS NOT NULL
) LOOP
COMMA_FINDER NUMBER := 1;
<<SEPARATE_ALTERNATENAMES>> WHILE COMMA_FINDER!=0 LOOP
INSERT INTO INFO_TABLE
(INFOID, NAME, LANGUAGE, STATUS, COUNTRY_CODE, COUNTRY_CODE_2, GID, SUPPLIERID, LAST_MODIFIED)
VALUES
(INFO_COUNTER.NEXTVAL, SUBSTR(REC.ALTERNATENAMES, INSTR(REC.ALTERNATENAMES, ',', COMMA_FINDER+1)), NULL, 'ALTERNATE', REC.COUNTRY_CODE, REC.COUNTRY_CODE_2, REC.GID, NULL, REC.LASTMODIFIED);
COMMA_FINDER := INSTR(REC.ALTERNATENAMES, ',', COMMA_FINDER);
END LOOP SEPARATE_ALTERNATENAMES;
END LOOP;
END
/
I hope that helps you proceed. I haven't test it and I'm afraid that SUBSTR will fail once it reaches the last name. But you'll figure that out.

Here is a little function I use to loop things like you are asking for. You can specify a delimiter.
The type...
type split_array is table of varchar2(32767) index by binary_integer;
The function...
function split(string_in varchar2, delim_in varchar2) return split_array is
i number :=0;
pos number :=0;
lv_str varchar2(32767) := string_in;
strings split_array;
dl number;
begin
-- determine first chuck of string
pos := instr(lv_str,delim_in,1,1);
-- get the length of the delimiter
dl := length(delim_in);
if (pos = 0) then --then we assume there is only 1 items in the list. so we just add the delimiter to the end which would make the pos length+1;
strings(1) := lv_str;
end if;
-- while there are chunks left, loop
while ( pos != 0) loop
-- increment counter
i := i + 1;
-- create array element for chuck of string
strings(i) := substr(lv_str,1,pos-1);
-- remove chunk from string
lv_str := substr(lv_str,pos+dl,length(lv_str));
-- determine next chunk
pos := instr(lv_str,delim_in,1,1);
-- no last chunk, add to array
if pos = 0 then
strings(i+1) := lv_str;
end if;
end loop;
-- return array
return strings;
end split;
How to use it...
declare
/* alternatenames varchar2(32767) := 'one,two,three,four'; */
nameArray split_array;
begin
for c1 in ( select alternatenames from yourTable where alternatenames is not null )
loop
nameArray := split(c1.alternatenames,',');
for i in 1..nameArray.count loop
/* dbms_output.put_line(nameArray(i)); */
insert into yourTable ( yourColumn ) values ( nameArray(i) );
end loop;
end loop;
end;
/

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;

In PL/SQL can I set a variable dynamically?

In Oracle 12c R2, I have a function which receives a row type as a variable. In the function I want to read a table which contains a column name and a value, I want to then populate the row type variable passed in using the column name and the data from the table I read.
Here is a simplistic idea of what I want to do;
CREATE TABLE table_to_be_updated
(
key_value number,
cola varchar2(2),
colb varchar2(2),
colc varchar2(2),
cold varchar2(2),
cole varchar2(2),
colf varchar2(2)
);
CREATE TABLE table_default_value
(
default_stuff number,
column_name varchar(30),
column_default_value varchar2(2)
);
function do_defaults(in_table table_to_be_updated%rowtype, in_value number) return table_to_be_updated%rowtype
is
out_table table_to_be_updated%rowtype := in_table;
cursor my_curs
is
select * from table_default_value where default_stuff = in_value;
begin
for default_rec in my_curs
loop
out_table.[default_rec.column_name] := default_rec.column_default_value
end loop;
return out_table;
end;
insert into table_default_value (default_stuff,column_name,column_default_value) values (1,'cola','xx'));
insert into table_default_value (default_stuff,column_name,column_default_value) values (1,'colc','aa'));
insert into table_default_value (default_stuff,column_name,column_default_value) values (1,'cole','bb'));
In the line;
out_table.[default_rec.column_name] := [default_rec.column_default_value]
[default_rec.column_name] would be the column name, from the cursor, in out_table name I want to move data to.
and
[default_rec.column_default_value] is the value from the cursor I want to move into that column.
I suspect that what I want to do is impossible in PL/SQL, but I thought I'd ask.
There are other ways to accomplish updating the table directly, specifically using dynamic SQL with execute immediate, but I have a number of similar tables which all need to have the same things done to them, and I would prefer a single function to work on a record and then pass it back to have the calling routine update the proper table.
Here is the best I can come up with;
function do_defaults(in_table table_to_be_updated%rowtype, in_value number) return table_to_be_updated%rowtype
is
TYPE DEFAULT_TYPE IS TABLE OF VARCHAR2(2)
INDEX BY VARCHAR2(30);
DEFAULT_ARRAY DEFAULT_TYPE;
out_table table_to_be_updated%rowtype := in_table;
cursor my_curs
is
select * from table_default_value where default_stuff = in_value;
begin
DEFAULT_ARRAY('cola') := null;
DEFAULT_ARRAY('colb') := null;
DEFAULT_ARRAY('colc') := null;
DEFAULT_ARRAY('cold') := null;
DEFAULT_ARRAY('cole') := null;
DEFAULT_ARRAY('colf') := null;
for default_rec in my_curs
loop
DEFAULT_ARRAY(default_rec.column_name) := default_rec.column_default_value
end loop;
out_table.cola := DEFAULT_ARRAY('cola');
out_table.colb := DEFAULT_ARRAY('colb');
out_table.colc := DEFAULT_ARRAY('colc');
out_table.cold := DEFAULT_ARRAY('cold');
out_table.cole := DEFAULT_ARRAY('cole');
out_table.colf := DEFAULT_ARRAY('colf');
return out_table;
end;

PLSQL : FORALL insert when data is NOT from the type

I want to use FORALL to insert data into a table. But, in my below code I will not be able to
get l_final_amt and l_reference_number variables outside the FOR loop of l_tbl_table_test_retrieve.
How to use FORALL to insert data into a table when values are not in the given type?
CREATE OR REPLACE PACKAGE test_FORALL AS
PROCEDURE pr_test_FORALL;
END test_FORALL;
CREATE OR REPLACE PACKAGE BODY test_FORALL AS
PROCEDURE pr_test_FORALL IS
TYPE ty_tbl_table_test IS TABLE OF table_test%ROWTYPE INDEX BY BINARY_INTEGER;
l_tbl_table_test_retrieve ty_tbl_table_test;
l_tbl_table_test ty_tbl_table_test;
l_final_amt INTEGER;
l_reference_number VARCHAR2(100);
BEGIN
SELECT * BULK COLLECT
INTO l_tbl_table_test_retrieve
FROM table_test t1;
FOR i IN 1 .. l_tbl_table_test_retrieve.COUNT
LOOP
l_tbl_table_test(l_tbl_table_test.COUNT + 1) := l_tbl_table_test_retrieve(i);
l_final_amt := l_final_amt + 10;
l_reference_number := SYSDATE + l_tbl_table_test_retrieve(i).ID;
insert into some_other_table(fname, address,final_amt,ref_number)
values(l_tbl_table_test_retrieve(i).fname, l_tbl_table_test_retrieve(i).address,l_final_amt,l_reference_number);
END LOOP;
--I want to insert into some_other_table using FORALL. But,l_final_amt and l_reference_number variables
-- are not available in l_tbl_table_test_retrieve.
EXCEPTION
DBMS_OUTPUT.put_line('EXCEPTION occurred');
END;
END pr_test_FORALL;
END test_FORALL;
Use a cursor and add the fields into the rows returned by the cursor:
PROCEDURE pr_test_FORALL IS
DECLARE csrData AS CURSOR FOR
SELECT t1.*,
NULL AS COUNT_VAL,
NULL AS FINAL_AMT,
NULL AS REFERENCE_NUMBER
FROM TABLE_TEST t1;
TYPE ty_tbl_table_test IS
TABLE OF csrData%ROWTYPE -- Note: csrData%ROWTYPE
INDEX BY BINARY_INTEGER;
l_tbl ty_tbl_table_test;
l_final_amt INTEGER := 0;
l_reference_number VARCHAR2(100);
BEGIN
OPEN csrData
FETCH csrData
BULK COLLECT INTO l_tbl;
CLOSE csrData;
FOR i IN 1 .. l_tbl.COUNT LOOP
l_final_amt := l_final_amt + 10;
l_tbl(i).FINAL_AMT := l_final_amt;
l_tbl(i).REFERENCE_NUMBER := SYSDATE + l_tbl(i).ID;
END LOOP;
FORALL i IN l_tbl.FIRST..l_tbl.LAST
INSERT INTO SOME_OTHER_TABLE
(FNAME, ADDRESS, FINAL_AMT, REF_NUMBER)
VALUES
(l_tbl(i).FNAME,
l_tbl(i).ADDRESS,
l_tbl(i).FINAL_AMT,
l_tbl(i).REFERENCE_NUMBER);
EXCEPTION
DBMS_OUTPUT.put_line('EXCEPTION occurred');
END pr_test_FORALL;
You could convert the whole thing into two inserts of the below form into the required tables.
I see that in your code l_reference_number is defined as a VARCHAR2 variable but it sounds like a number. ( SYSDATE + some_number ) will yield a date type. It will be implicitly converted into a string based on your NLS_ settings when you assign it to a varchar2. I'm not sure what do you want to store in there as a "REFERENCE_NUMBER".
INSERT INTO some_other_table (
fname,
address,
final_amt,
ref_number
)
SELECT fname,
address,
10 * ROWNUM AS final_amt,
SYSDATE + id as reference_number
FROM table_test;

How to access and query objects passed as parameter to a procedure while converting from Oracle to postgresql

I have a procedure in Oracle that I need to convert to Postgresql and need help on it. It paases a collection of objects in a procedure.The procedure then checks if each object is present in a database table or not and if present it gives a message that , that specific element is found/present. if some element that is paassed to the procedure is not present in the table, the procedure just doesnt do anything. I have to write equivalent of that in postgresql. I think the heart of the issue is this statement:
SELECT COUNT (*)
INTO v_cnt
FROM **TABLE (p_cust_tab_type_i)** pt
WHERE pt.ssn = cc.ssn;
In Oracle a collection can be treated as a table and one can query it but I dont know how to do that in postgresql. The code to create the table, add data, create the procedure, call the procedure by passing the collection (3 objects) and output of that is posted below. Can someone suggest how this can be done in postgresql?
Following the oracle related code and details:
--create table
create table temp_n_tab1
(ssn number,
fname varchar2(20),
lname varchar2(20),
items varchar2(100));
/
--add data
insert into temp_n_tab1 values (1,'f1','l1','i1');
--SKIP no. ssn no. 2 intentionally..
insert into temp_n_tab1 values (3,'f3','l3','i3');
insert into temp_n_tab1 values (4,'f4','l4','i4');
insert into temp_n_tab1 values (5,'f5','l5','i5');
insert into temp_n_tab1 values (6,'f6','l6','i6');
commit;
--create procedure
SET SERVEROUTPUT ON
CREATE OR REPLACE PROCEDURE temp_n_proc (
p_cust_tab_type_i IN temp_n_customer_tab_type)
IS
t_cust_tab_type_i temp_n_customer_tab_type;
v_cnt NUMBER;
v_ssn temp_n_tab1.ssn%TYPE;
CURSOR c
IS
SELECT ssn
FROM temp_n_tab1
ORDER BY 1;
BEGIN
--t_cust_tab_type_i := p_cust_tab_type_i();
FOR cc IN c
LOOP
SELECT COUNT (*)
INTO v_cnt
FROM TABLE (p_cust_tab_type_i) pt
WHERE pt.ssn = cc.ssn;
IF (v_cnt > 0)
THEN
DBMS_OUTPUT.put_line (
'The array element '
|| TO_CHAR (cc.ssn)
|| ' exists in the table.');
END IF;
END LOOP;
EXCEPTION
WHEN OTHERS
THEN
DBMS_OUTPUT.PUT_LINE (SQLERRM);
END;
/
--caller proc
SET SERVEROUTPUT ON
declare
array temp_n_customer_tab_type := temp_n_customer_tab_type();
begin
for i in 1 .. 3
loop
array.extend;
array(i) := temp_n_cust_header_type( i, 'name ' || i, 'lname ' || i,i*i*i*i );
end loop;
temp_n_proc( array );
end;
/
caller proc output:
The array element 1 exists in the table.
The array element 3 exists in the table.
When you create a table in Postgres, a type with the same name is also created. So you can simply pass an array of the table's type as a parameter to the function.
Inside the function you can then use unnest() to treat the array like a table.
The following is the closest match to your original Oracle code:
create function temp_n_proc(p_cust_tab_type_i temp_n_tab1[])
returns void
as
$$
declare
l_rec record;
l_msg text;
l_count integer;
BEGIN
for l_rec in select t1.ssn
from temp_n_tab1 t1
loop
select count(*)
into l_count
from unnest(p_cust_tab_type_i) as t
where t.ssn = l_rec.ssn;
if l_count > 0 then
raise notice 'The array element % exist in the table', l_rec.ssn;
end if;
end loop;
END;
$$
language plpgsql;
The row-by-row processing is not a good idea to begin with (neither in Postgres, nor in Oracle). It would be a lot more efficient to get the existing elements in a single query:
create function temp_n_proc(p_cust_tab_type_i temp_n_tab1[])
returns void
as
$$
declare
l_rec record;
l_msg text;
BEGIN
for l_rec in select t1.ssn
from temp_n_tab1 t1
where t1.ssn in (select t.ssn
from unnest(p_cust_tab_type_i) as t)
loop
raise notice 'The array element % exist in the table', l_rec.ssn;
end loop;
return;
END;
$$
language plpgsql;
You can call the function like this:
select temp_n_proc(array[row(1,'f1','l1','i1'),
row(2,'f2','l2','i2'),
row(3,'f3','l3','i3')
]::temp_n_tab1[]);
However a more "Postgres" like and much more efficient way would be to not use PL/pgSQL for this, but create a simple SQL function that returns the messages as a result:
create or replace function temp_n_proc(p_cust_tab_type_i temp_n_tab1[])
returns table(message text)
as
$$
select format('The array element %s exist in the table', t1.ssn)
from temp_n_tab1 t1
where t1.ssn in (select t.ssn
from unnest(p_cust_tab_type_i) as t)
$$
language sql;
This returns the output of the function as a result rather than using the clumsy raise notice.
You can use it like this:
select *
from temp_n_proc(array[row(1,'f1','l1','i1'),
row(2,'f2','l2','i2'),
row(3,'f3','l3','i3')
]::temp_n_tab1[]);

PL SQL : Create oracle record dynamically

I dont know if it is possible but I would like to do this in PL/SQL
Let's say I have a parameter in my procedure, a number : numberColumns.
Inside the procedure I would like to create a record :
TYPE arrayColumn IS RECORD (
column1 VARCHAR2(200),
column2 VARCHAR2(200)...
... as much à numberColumns value
....
);
ty
This can be achieved easily with with OBJECT type in Oracle. But this kind of architecture is not at all suggested. Hope this below solution helps.
CREATE OR REPLACE
PROCEDURE test_obj_form(
a NUMBER )
AS
lv_sql VARCHAR2(32676);
BEGIN
SELECT '('
||listagg(str,',') WITHIN GROUP (
ORDER BY lvl)
||')'
INTO lv_sql
FROM
(SELECT 1 dum,
'column'
||LEVEL
||' '
||'varchar2(200)' str,
level lvl
FROM dual
CONNECT BY LEVEL < A
ORDER BY LEVEL
)
GROUP BY dum;
EXECUTE IMMEDIATE 'CREATE OR REPLACE TYPE name_rec IS OBJECT '||lv_sql;
dbms_output.put_line(lv_sql);
END;
/
Rather than using a record, you could use a collection:
CREATE OR REPLACE TYPE stringlist IS TABLE OF VARCHAR2(4000);
/
Then do:
CREATE OR REPLACE PROCEDURE your_procedure(
number_columns IN INTEGER,
values OUT stringlist
)
IS
BEGIN
values := stringlist();
IF number_columns < 1 THEN
RETURN;
END IF;
values.EXTEND( number_columns );
FOR i IN 1 .. number_columns LOOP
values(i) := DBMS_RANDOM.STRING( 'X', 100 ); -- Assign some value
END LOOP;
END;
/
Otherwise, if you really want a record, then you will have to result to dynamic SQL.

Resources