Passing values to IN clause on a function oracle - sql

I need to return a cursor within a function:
CREATE OR REPLACE FUNCTION test_cursor (
bigstring IN VARCHAR2
)
RETURN cursor
IS
row_test table_colors := table_colors(bigstring);
c1 CURSOR;
BEGIN
OPEN c1 FOR
select * from cars where color IN (select column_value
from table(row_test));
RETURN c1;
END test_cursor;
table_colors is:
create or replace type table_colors as table of varchar2(20);
But when I test it passing like blue, red, pink, white or 'blue', 'red', 'pink', 'white' always throws the same error
ORA-06502: PL/SQL; numeric or value error: character string buffer too small
on this line row table_colors := table_colors(bigstring);
What I am doing wrong here?

The problem is that bigstring is a single scalar value that may happen to contain commas and single quotes not a list of values. You would need to parse the string to extract the data elements. If each of the individual elements within bigstring happens to be a valid Oracle identifier, you could use the built-in dbms_utility.comma_to_table function. Were it my system, though, I'd feel more comfortable with my own parsing function. Assuming that bigstring is just a comma-separated list, I'd use a version of Tom Kyte's str2tbl function
create or replace function str2tbl( p_str in varchar2 )
return table_colors
as
l_str long default p_str || ',';
l_n number;
l_data table_colors := table_colors();
begin
loop
l_n := instr( l_str, ',' );
exit when (nvl(l_n,0) = 0);
l_data.extend;
l_data( l_data.count ) := ltrim(rtrim(substr(l_str,1,l_n-1)));
l_str := substr( l_str, l_n+1 );
end loop;
return l_data;
end;
Now, you can realistically implement str2tbl using regular expressions in a single SQL statement as well. That might be a touch more efficient. I'd expect, however, that string parsing is well down on your list of performance issues so I would tend to stick with the simplest thing that could possibly work.
Your procedure would then become
CREATE OR REPLACE FUNCTION test_cursor (
bigstring IN VARCHAR2
)
RETURN sys_refcursor
IS
row_test table_colors := str2tbl(bigstring);
c1 sys_refcursor;
BEGIN
OPEN c1 FOR
select * from cars where color IN (select column_value
from table(row_test));
RETURN c1;
END test_cursor;

Please show the definition of table_colors. It appears that table_colors(bigstring) is returning a value incompatible with assignment to table_colors.
As a matter of good practice, initialization of non trivial values should be done inside of the begin ... end rather than in the definition section. That allows you to trap the error within the function or procedure rather than the error cascading outwards. For example, rather than:
IS
row_test table_colors := table_colors(bigstring);
c1 CURSOR;
BEGIN ...
You should use
IS
row_test table_colors;
c1 CURSOR;
BEGIN
row_test := row_test;
...

Related

Encrypting strings in SQL (likely ORACLE PL SQL). Caesar Cipher

I need something like the Caesar Cipher to be used in my string columns for the every value in each column. It should be made something like n+1:
ABcd012Ab -> BCde123Bc
The string characters may be null, may contain sepparators (, - etc.), they may be upper and lower case (it doesnt matter).
Finaly, it should be created as a procedure, and this procedure should be then used inside an UPDATE query.
It shold maybe look something like this:
Create procedure text_change(n varchar(1000))
declare #i char
declare #l varchar(100 char)
begin
For each #l in n
For each #i in #l
loop
#i = ????
end loop;
return #l;
end;
UPDATE name_of_table
SET name_of_column = text_change(column)
Would be very happy for any help!
Why restrict yourself to Caesar Cipher? You could make use of DBMS_CRYPTO package which allows you to use Data Encryption Standard (DES)
Docs
Firstly, get execute permission to this package from DBA.
SQL> GRANT EXECUTE ON DBMS_CRYPTO TO HR;
Grant succeeded.
Then create a function like this.
CREATE OR REPLACE FUNCTION my_encrypt(
p_source VARCHAR2,
p_key VARCHAR2 )
RETURN VARCHAR2
AS
BEGIN
RETURN UTL_RAW.CAST_TO_VARCHAR2 ( DBMS_CRYPTO.encrypt( UTL_RAW.CAST_TO_RAW (p_source),
dbms_crypto.DES_CBC_PKCS5, UTL_RAW.CAST_TO_RAW (p_key) ) );
END;
/
This uses DES_CBC_PKCS5 Block Cipher Suite.
So, when you run a query like this you get encrypted data.
SQL> SELECT my_encrypt('TREASURE UNDER OAK TREE',
2 'The DBMS_CRYPTO package replaces DBMS_OBFUSCATION_TOOLKIT') AS
3 encrypted
4 FROM dual;
ENCRYPTED
----------------------------
┐↨┐┐♣!┐ o)|┐┐┐┐┐┐┐┐
Decrypt function
CREATE OR REPLACE FUNCTION my_decrypt ( p_source VARCHAR2, p_key VARCHAR2 )
RETURN VARCHAR2 AS
BEGIN
RETURN UTL_RAW.CAST_TO_VARCHAR2 ( DBMS_CRYPTO.decrypt( UTL_RAW.CAST_TO_RAW (p_source), dbms_crypto.DES_CBC_PKCS5, UTL_RAW.CAST_TO_RAW (p_key) ) );
END;
/
SQL> SELECT my_decrypt( my_encrypt('TREASURE UNDER OAK TREE',
2 'The DBMS_CRYPTO package replaces DBMS_OBFUSCATION_TOOLKIT') ,
3 'The DBMS_CRYPTO package replaces DBMS_OBFUSCATION_TOOLKIT') AS
4 decrypted
5 FROM dual;
DECRYPTED
---------------------------------
TREASURE UNDER OAK TREE
You could also use it to encrypt and decrypt the columns in the table.
update yourtable set SOMETEXT =
my_encrypt(SOMETEXT,'The DBMS_CRYPTO package replaces DBMS_OBFUSCATION_TOOLKIT');
update yourtable set SOMETEXT =
my_decrypt(SOMETEXT,'The DBMS_CRYPTO package replaces DBMS_OBFUSCATION_TOOLKIT');
If you only want to "encrypt" alphanumerics, you might try the following. You didn't specify what you wanted done with 9, Z, or z so I just took the next ASCII character (:, [, and { respectively):
SELECT mycolumn
, TRANSLATE( mycolumn
, '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
, '123456789:BCDEFGHIJKLMNOPQRSTUVWXYZ[bcdefghijklmnopqrstuvwxyz{' )
FROM mytable;
Hope this helps.
EDIT: I'm not sure why I've continued to think about this, but here is a general solution with user-defined function using Oracle's TRANSLATE() function. It doesn't include numbers but I'm sure those would be an easy addition:
CREATE OR REPLACE FUNCTION caesar_cipher
( p_source IN VARCHAR2, p_offset IN PLS_INTEGER )
RETURN VARCHAR2
IS
c_abc CONSTANT VARCHAR2(128) := 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
v_offset PLS_INTEGER;
v_target VARCHAR2(32767);
v_transl VARCHAR2(128);
BEGIN
v_offset := MOD( p_offset, LENGTH(c_abc) );
IF (v_offset < 0) THEN
v_offset := v_offset + LENGTH(c_abc);
END IF;
v_transl := SUBSTR(c_abc, v_offset+1) || SUBSTR(c_abc, 1, v_offset);
v_target := TRANSLATE( p_source, c_abc || LOWER(c_abc), v_transl || LOWER(v_transl) );
RETURN v_target;
END;
/
To "decrypt" use a negative value for the offset instead of a positive one; that is, CAESAR_CIPHER('CDE', -2) is the opposite of CAESAR_CIPHER('ABC', 2). It strikes me as more efficient than examining every character of the source string but I've not run a test to be sure.

PL-SQL stored procedure split string

What I would like to do is this:
In java I have an array like ['AB','BC','CD','DE'] which I want to concat to something like "AB,BC,CD,DE" and send it to the procedure as an argument.
In the procedure, my idea is, I would like to do something like
v_passedArgs --(AB,BC,CD,DE)
SELECT * FROM SOME_TABLE WHERE SOME_COL IN (v_passedArgs.split(','))
Is it possible to do something like that or maybe you have another idea?
Thank you
You can create a split function in the database and use it for splitting a string in SQL which has delimiters(',' in your example)
Is there a function to split a string in PL/SQL?
Refer the above link to create a split function and use it in your select statement
You have to create your own function.
You can work with an Oracle PL/SQL collection; here is a piece of code to return such a collection, from an input string list (p_list) with a given separator (p_sep):
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 use it with the TABLE keyword in your SELECT - the TABLE keyword converts the collection into a object usable inside Oracle SQL queries:
SELECT * FROM SOME_TABLE WHERE SOME_COL IN (
select * from TABLE(cto_table(',', v_passedArgs))
)

Manipulating collections

I try to sort a list of distinct elements (owner, table).
It's easy (and very quick to!) with just one, for example:
declare
TYPE tbl_list IS TABLE OF VARCHAR2(64);
l_tables tbl_list;
i number;
begin
l_tables:=tbl_list();
for i in 1..100000
loop
l_tables:= l_tables MULTISET UNION DISTINCT tbl_list('myTable');
end loop;
for i in l_tables.first.. l_tables.last
loop
dbms_output.put_line(l_tables(i));
end loop;
end;
/
I try to so the same with a list but it's failed:
create or replace TYPE tbl_list2 IS OBJECT (l_owner VARCHAR2(64),l_name VARCHAR2(64));
declare
l_object tbl_list2;
i number;
begin
l_object:=tbl_list2('','');
for i in 1..100000
loop
l_object:= l_object MULTISET UNION DISTINCT tbl_list2('myOwner','MyTable');
end loop;
for i in l_object.first.. l_object.last
loop
dbms_output.put_line(l_object(i));
end loop;
end;
/
But I catch the following:
PLS-00306: wrong number or types of arguments in call to 'MULTISET_UNION_DISTINCT'
The goal is to have a list of all distinct (owner, tables), I don't care if you find any others idea of course.
A solution is of course a concatenation in one word of the two, but I would like to find more elegant!
EDIT
#ThinkJet:
I love your solution. It's more elegant than my dirty solution.
But, Your solution is bout 70 time slower than mine!
So How could we converge to have a elegant ant speed solution?
Here my dirty one:
declare
TYPE tbl_list IS TABLE OF VARCHAR2(64);
l_tables tbl_list;
i number;
begin
l_tables:=tbl_list();
for i in 1..100000
loop
l_tables:= l_tables MULTISET UNION DISTINCT tbl_list('myOwner'||','||'myTable');
end loop;
for i in l_tables.first.. l_tables.last
loop
dbms_output.put_line('OWNER='||REGEXP_SUBSTR(l_tables(i),'[^,]+', 1, 1));
dbms_output.put_line('TABLE='||REGEXP_SUBSTR(l_tables(i),'[^,]+', 1, 1));
end loop;
end;
/
At least you lost a table definition in second case. This statement:
create or replace TYPE tbl_list2 IS OBJECT (l_owner VARCHAR2(64),l_name VARCHAR2(64));
declares only object (or record) type, not a table.
So you need to do it in 2 steps:
create or replace TYPE tbl_list_rec IS OBJECT (l_owner VARCHAR2(64),l_name VARCHAR2(64));
/
create or replace TYPE tbl_list2 as table of tbl_list_rec;
/
After that you need some syntax corrections in script:
declare
l_object tbl_list2;
i number;
begin
-- for list initialization it must be filled with constructed objects
l_object := tbl_list2( tbl_list_rec('','') );
for i in 1..100000 loop
-- 1. select values to variable
-- 2. Fix constructor for list
select
l_object MULTISET UNION DISTINCT tbl_list2(tbl_list_rec('myOwner','MyTable'))
into
l_object
from
dual;
end loop;
for i in l_object.first .. l_object.last loop
-- output separate fields, there are now default conversion from
-- user-defined objects to varchar2.
dbms_output.put_line(l_object(i).l_owner || ',' || l_object(i).l_name);
end loop;
end;
/
UPDATE
Solution above relatively slow because of big number of context switches. But comparison of complex object type instances can't be done directly in PL/SQL without some additional work.
To allow Oracle to know if object instances are same or different, we need to define mapping or ordering method for object type. Both types of methods not allowed, so there are need to choose proper one. MAP methods performs faster and there are no need for ordering in our case, so go for it:
create or replace TYPE tbl_list_rec2 AS OBJECT (
l_owner VARCHAR2(64),
l_name VARCHAR2(64),
map member function get_key return varchar2
);
/
Implementation:
create or replace TYPE BODY tbl_list_rec2 AS
map member function get_key return varchar2
is
begin
return l_owner||chr(1)||l_name;
end;
end;
/
After that it's possible to test objects for equality in PL/SQL code like simple varchar2 in first example from question:
declare
l_object tbl_list2a;
i number;
begin
l_object := tbl_list2a( tbl_list_rec2('','') );
for i in 1..100000 loop
l_object := l_object MULTISET UNION DISTINCT tbl_list2a(tbl_list_rec2('myOwner','MyTable'));
end loop;
for i in l_object.first.. l_object.last loop
dbms_output.put_line(l_object(i).l_owner || ',' || l_object(i).l_name);
end loop;
end;
/

how to sort the contents of CLOB field

I have a table and some fields are CLOB type and the content in the CLOB was delimited by some separator such as '|' and usually the content in the filed looks like this : name2|name1|name3..., actually the length of the content is more than 40000 characters, so is there any way to sort the content by asc? I want to look the content like this: name1|name2|name3...
can any body help me?
If it's even remotely possible, I'd strongly suggest you change your data model - add a details table for the names. This will solve you a lot of pain in the future.
Anyhow, if you absolutely need to store a pipe-separated list of names in your CLOB field, I'd suggest this approach:
break the CLOB into separate rows (using a pipelined function)
sort the rows
aggregate the rows into a new CLOB
A (somewhat naive and untested) implementation of this approach:
create type stringtabletype as table of varchar2(4000);
create or replace function split_CLOB(p_Value in CLOB,
p_Separator in varchar2 default '|')
return stringtabletype
pipelined as
l_Offset number default 1;
l_Str varchar2(4000);
idx number;
begin
idx := dbms_lob.instr(lob_loc => p_Value,
pattern => p_Separator,
offset => l_Offset);
dbms_output.put_line(idx);
while (idx > 0)
loop
l_Str := dbms_lob.substr(p_Value,
idx - l_Offset,
l_Offset);
pipe row(l_Str);
l_Offset := idx+1;
idx := dbms_lob.instr(p_Value,
p_Separator,
l_Offset);
dbms_output.put_line(idx);
end loop;
-- pipe remainder of string
l_Str := dbms_lob.substr(p_Value,
dbms_lob.getlength(p_Value) - l_Offset + 1,
l_Offset);
pipe row(l_str);
return;
end;
create or replace function sort_stringtabletype(p_Values in stringtabletype)
return stringtabletype as
l_Result stringtabletype;
begin
select column_value bulk collect
into l_Result
from table(p_Values)
order by column_value;
return l_Result;
end;
create or replace function stringtabletype_to_CLOB(p_Values in stringtabletype,
p_Separator in varchar2 default '|')
return CLOB as
l_Result CLOB;
begin
dbms_lob.createtemporary(l_Result, false);
for i in 1 .. p_Values.count - 1
loop
dbms_lob.writeappend(l_Result,
length(p_Values(i)),
p_Values(i));
dbms_lob.writeappend(l_Result,
length(p_Separator),
p_Separator);
end loop;
dbms_lob.writeappend(l_Result,
length(p_Values(p_Values.count)),
p_Values(p_Values.count));
return l_Result;
end;
Example usage:
select stringtabletype_to_CLOB (
sort_stringtabletype(
split_CLOB('def|abc|ghic', '|')
)
) from dual
You could then use an UPDATE statement like
update my_table
set clob_field = stringtabletype_to_CLOB (
sort_stringtabletype(
split_CLOB(my_table, '|')
)

Looping on values, creating dynamic query and adding to result set

I have the following problem. I am an experienced Java programmer but am a bit of a n00b at SQL and PL/SQL.
I need to do the following.
1 Pass in a few arrays and some other variables into a procedure
2 Loop on the values in the arrays (they all have the same number of items) and dynamically create an SQL statement
3 Run this statement and add it to the result set (which is an OUT parameter of the procedure)
I already have experience of creating an SQL query on the fly, running it and adding the result to a result set (which is a REF CURSOR) but I'm not sure how I'd loop and add the results of each call to the query to the same result set. I'm not even sure if this is possible.
Here's what I have so far (code edited for simplicity). I know it's wrong because I'm just replacing the contents of the RESULT_SET with the most recent query result (and this is being confirmed in the Java which is calling this procedure).
Any and all help would be greatly appreciated.
TYPE REF_CURSOR IS REF CURSOR;
PROCEDURE GET_DATA_FASTER(in_seq_numbers IN seq_numbers_array, in_values IN text_array, in_items IN text_array, list IN VARCHAR2, RESULT_SET OUT REF_CURSOR) AS
query_str VARCHAR2(4000);
seq_number NUMBER;
the_value VARCHAR2(10);
the_item VARCHAR2(10);
BEGIN
FOR i IN 1..in_seq_numbers.COUNT
LOOP
seq_number := in_seq_numbers(i);
the_value := trim(in_values(i));
the_item := trim(in_items(i));
query_str := 'SELECT distinct '||seq_number||' as seq, value, item
FROM my_table ai';
query_str := query_str || '
WHERE ai.value = '''||the_value||''' AND ai.item = '''||the_item||'''
AND ai.param = ''BOOK''
AND ai.prod in (' || list || ');
OPEN RESULT_SET FOR query_str;
END LOOP;
EXCEPTION WHEN OTHERS THEN
RAISE;
END GET_DATA_FASTER;
A pipelined table function seems a better fit for what you want, especially if all you're doing is retrieving data. See http://www.oracle-base.com/articles/misc/pipelined-table-functions.php
What you do is create a type for your output row. So in your case you would create an object such as
CREATE TYPE get_data_faster_row AS OBJECT(
seq NUMBER(15,2),
value VARCHAR2(10),
item VARCHAR2(10)
);
Then create a table type which is a table made up of your row type above
CREATE TYPE get_data_faster_data IS TABLE OF get_data_faster_row;
Then create your table function that returns the data in a pipelined manner. Pipelined in Oracle is a bit like a yield return in .net (not sure if you're familiar with that). You find all of the rows that you want and "pipe" them out one at a time in a loop. When your function completes the table that's returned consists of all the rows you piped out.
CREATE FUNCTION Get_Data_Faster(params) RETURN get_data_faster_data PIPELINED AS
BEGIN
-- Iterate through your parameters
--Iterate through the results of the select using
-- the current parameters. You'll probably need a
-- cursor for this
PIPE ROW(get_data_faster_row(seq, value, item));
LOOP;
LOOP;
END;
EDIT: Following Alex's comment below, you need something like this. I haven't been able to test this but it should get you started:
CREATE FUNCTION Get_Data_Faster(in_seq_numbers IN seq_numbers_array, in_values IN text_array, in_items IN text_array, list IN VARCHAR2) RETURN get_data_faster_data PIPELINED AS
TYPE r_cursor IS REF CURSOR;
query_results r_cursor;
results_out get_data_faster_row := get_data_faster_row(NULL, NULL, NULL);
query_str VARCHAR2(4000);
seq_number NUMBER;
the_value VARCHAR2(10);
the_item VARCHAR2(10);
BEGIN
FOR i IN 1..in_seq_number.COUNT
LOOP
seq_number := in_seq_numbers(i);
the_value := trim(in_values(i));
the_item := trim(in_items(i));
query_str := 'SELECT distinct '||seq_number||' as seq, value, item
FROM my_table ai';
query_str := query_str || '
WHERE ai.value = '''||the_value||''' AND ai.item = '''||the_item||'''
AND ai.param = ''BOOK''
AND ai.prod in (' || list || ');
OPEN query_results FOR query_str;
LOOP
FETCH query_results INTO
results_out.seq,
results_out.value,
results_out.item;
EXIT WHEN query_results%NOTFOUND;
PIPE ROW(results_out);
END LOOP;
CLOSE query_results;
END LOOP;
END;
Extra info from Alex's comment below useful for the answer:
you can have multiple loops from different sources, and as long as the
data from each be put into the same object type, you can just keep
pumping them out with pipe row statements anywhere in the function.
The caller sees them as a table with the rows in the order you pipe
them. Rather than call a procedure and get a result set as an output
parameter, you can query as select seq, value, item from
table(package.get_data_faster(a, b, c, d)), and of course you can
still have an order by clause if the order they're piped isn't what
you want.