pl/sql save hierarchical query to variable - oracle

I am trying to save result set of hierarchical query to variable
CREATE OR REPLACE FUNCTION test12
RETURN number IS
result number(4):=0;
clli_array dbms_sql.varchar2_table;
BEGIN
with tmp as (select 'strforregexp' str from dual)
select regexp_substr(str, '\/([A-Z0-9]{11}|[A-Z0-9]{8})', 1, level) STR into :clli_array from tmp
connect by regexp_substr(str, '\/([A-Z0-9]{11}|[A-Z0-9]{8})', 1, level) is not null;
END test12;
But getting an error
Error(9,9): PLS-00049: bad bind variable 'CLLI_ARRAY'
So, i have 2 questions
1) can i get all matches of regexp witohout hierarchical query
2) why i am getting an error

As #APC pointed out, the first problem is that you've got a colon in front of CLLI_ARRAY. This causes the PL/SQL compiler to believe that CLLI_ARRAY is going to be a SQL*Plus substitution variable, and when it finds that such a variable is not defined it throws the error you got.
However, even if you remove the colon you're not out of the woods yet. Once you remove the colon you'll get
PLS-00597: expression 'CLLI_ARRAY' in the INTO list is of wrong type
That's because CLLI_ARRAY is a PL/SQL-type collection, but your statement returns a single string.
What you probably want to do is to use BULK COLLECT to have the system retrieve all the results of the query into your VARCHAR2_TABLE:
with tmp as (select 'strforregexp' str from dual)
select regexp_substr(str, '\/([A-Z0-9]{11}|[A-Z0-9]{8})', 1, level) STR
BULK COLLECT into clli_array
from tmp
connect by regexp_substr(str, '\/([A-Z0-9]{11}|[A-Z0-9]{8})', 1, level) is not null
Best of luck.

Related

ORA-00936 with a substitution variable - error does not make sense

A super simple example of my script looks as follows:
-- Report Name: "Report_1"
col letters new_value p_letters
SELECT letters
FROM param_table
WHERE report_name = 'Report_1';
CREATE TABLE temp_table_1
(letter varchar2(1));
INSERT INTO temp_table_1(letter)
SELECT DISTINCT letter
FROM table_alphabet
WHERE '&&p_letters' = '' OR letter IN (&&p_letters);
For some reason, our system has a table called param_table: users enter parameters through the UI, the parameters entered are written to param_table, and then my script pulls the user's parameters from param_table.
As far as I understand, the first SELECT statement selects the letters column from param_table and makes its values accessible in '&&p_letters'. In my INSERT INTO statement, when my WHERE clause looks like this...
WHERE letter IN (&&p_letters);
...and the user enters letters separated by single quotes, eg ('A', B', C'), the script runs fine. I want to make the parameter optional, so I adjusted the WHERE clause like this:
WHERE '&&p_letters' = '' OR letter IN (&&p_letters);
In my output file, I get the following error:
WHERE (('' = '') OR letter IN ()) *
ERROR at line ...:
ORA-00936: missing expression
The compiler has evaluated the substitution variable correctly as '', but I'm getting an error.
Any idea what I could be doing wrong here?
The ORA-00936 is because IN () is not valid - you're missing something inside that. It is that it is complaining about, not the '' = '' part, though the result of that is undefined. You can check both conditions:
SQL> select * from dual where '' = '';
no rows selected
SQL> select * from dual where dummy in ();
select * from dual where dummy in ()
*
ERROR at line 1:
ORA-00936: missing expression
If you set verify on you can see how the substitution is handled. For your original query you'd see:
old:INSERT INTO temp_table_1(letter)
SELECT DISTINCT letter
FROM table_alphabet
WHERE letter IN (&&p_letters)
new:INSERT INTO temp_table_1(letter)
SELECT DISTINCT letter
FROM table_alphabet
WHERE letter IN ('A','B','C')
3 rows inserted.
You can see that the post-substitution statement looks, and is, valid.
With your modified query you'd see:
old:INSERT INTO temp_table_1(letter)
SELECT DISTINCT letter
FROM table_alphabet
WHERE '&&p_letters' = '' OR letter IN (&&p_letters)
new:INSERT INTO temp_table_1(letter)
SELECT DISTINCT letter
FROM table_alphabet
WHERE ''A','B','C'' = '' OR letter IN ('A','B','C')
which generates an ORA-00920 because of the messed-up single quotes in the first expression. With no value from letters you'd instead see:
old:INSERT INTO temp_table_1(letter)
SELECT DISTINCT letter
FROM table_alphabet
WHERE '&&p_letters' = '' OR letter IN (&&p_letters)
new:INSERT INTO temp_table_1(letter)
SELECT DISTINCT letter
FROM table_alphabet
WHERE '' = '' OR letter IN ()
which is the error you saw, ORA-00936.
I'd be tempted to do this with a collection type, either your own, or if you're comfortable with it then a built-in one:
INSERT INTO temp_table_1(letter)
SELECT DISTINCT letter
FROM table_alphabet
WHERE SYS.DBMS_DEBUG_VC2COLL(&&p_letters) IS EMPTY
OR letter MEMBER OF SYS.DBMS_DEBUG_VC2COLL(&&p_letters);
That works with your three comma-separated values, or null, since an empty collection is allowed. Read more about is empty and member of.
It would be better, of course, to not store comma-separated lists in a single column value anyway, and to change your data model so this kind of manipulation and reliance on client behaviour isn't necessary.
Assuming you're stuck with the data model, you could at least avoid the client reliance buy tokenizing the string (I'm using one common approach below) and looking for matches. However, you also need to account for either the report name not being in the table at all or the report existing with no letters value, both of which are handled by the max(letters) .. is null check - which makes it a bit ugly.
It's all in one statement though, with no need for a separate query to get the parameters and no need for substitution variables. (And there may be better ways to do it!)
INSERT INTO temp_table_1 (letter)
SELECT DISTINCT letter
FROM table_alphabet
WHERE (
SELECT MAX(letters)
FROM param_table
WHERE report_name = 'Report_2'
) IS NULL
OR letter IN (
SELECT TRIM(q'[']' FROM REGEXP_SUBSTR(letters, '[^,]', 1, LEVEL))
FROM param_table
WHERE report_name = 'Report_2'
CONNECT BY REGEXP_SUBSTR(letters, '[^,]', 1, level) IS NOT NULL
);

How to chunk a string in pl sql using regexp?

I have a string as follows: ABCAPP9 Xore-Done-1. I want to chunk the string to get 4 elements separately at a given time in pl sql. Pls tell me the 4 different queries to get the following 4 results separately. Thanks
ABCAPP9
Xore
Done
1
REGEXP_SUBSTR ('ABCAPP9 Xore-Done-1', '[^[:space:]-]+', 1, n)
will give you the n-th part. Change n with the number you want. Here are all:
SELECT REGEXP_SUBSTR ('ABCAPP9 Xore-Done-1', '[^[:space:]-]+', 1, LEVEL)
FROM DUAL
CONNECT BY REGEXP_SUBSTR ('ABCAPP9 Xore-Done-1', '[^[:space:]-]+', 1, LEVEL) IS NOT NULL
This should be a comment really to #Mottor but due to no formatting in comments I need to make it here.
A word of warning. As long as all elements of your string will be present and the delimiters can NEVER be next to each other you will be ok. However, the regex format of '[^<delimiter>]+' commonly used for parsing strings will not return the correct value if there is a NULL element in the list! See this post for proof: https://stackoverflow.com/a/31464699/2543416. To test in your example, remove the substring "Xore", leaving the space and hyphen next to each other:
SQL> SELECT REGEXP_SUBSTR ('ABCAPP9 -Done-1', '[^[:space:]-]+', 1, LEVEL)
FROM DUAL
CONNECT BY REGEXP_SUBSTR ('ABCAPP9 -Done-1', '[^[:space:]-]+', 1, LEVEL) IS NOT NULL;
REGEXP_SUBSTR('
---------------
ABCAPP9
Done
1
The 2nd element should be NULL, but "Done" is returned instead! Not good if the position is important.
Use this format instead to handle NULLs and return the correct string element in the correct position (shown here with "Xore" removed and thus a NULL returned in that position to prove it handles the NULL):
SQL> with tbl(str) as (
select 'ABCAPP9 -Done-1' from dual
)
select regexp_substr(str, '(.*?)( |-|$)', 1, level, NULL, 1)
from tbl
connect by regexp_substr(str, '(.*?)( |-|$)', 1, level) is not null;
REGEXP_SUBSTR(S
---------------
ABCAPP9
Done
1
SQL>
I shudder to think of all the bad data being returned out there.
So user2153047, if you are still with me, for your need if you want the 3rd element (and handle the NULL) you would use:
SQL> select regexp_substr('ABCAPP9 -Done-1', '(.*?)( |-|$)', 1, 3, NULL, 1) "3rd"
from dual;
3rd
----
Done

Oracle invalid number in clause

I'm struggling with getting a query to work, and I could really use some help. We have an in house app that we use for building small web apps where I work. It's basically a drag and drop GUI. There's functionality built in to access query string values using the key.
I'm passing a comma separated list of values into a page through the query string. I'm then trying to use the list of values as part of an in clause in a query.
I can see that the value is correct in the query string.
orders=1,2,3
Here's the specific part of the query
"AND OrderNumber IN (this is where it maps from the query string)
I've tried running similar queries in Toad, and I think I've found the issue. It's giving an invalid number error, and I think it's wrapping the query string value in single quotes. I can replicate the error when I do "AND OrderNumber IN ('1,2,3')" in Toad.
Here's where I get really confused. The following works in Toad.
"AND OrderNumber IN ('1','2','3')"
So I tried recreating that by doing
select replace('1,2,3', ',', chr(39)||','||chr(39)) from dual;
I have confirmed that returns '1','2','3' in Toad.
However, I still get an Invalid Number error when I run the following in Toad.
AND OrderNumber IN (replace('1,2,3', ',', chr(39)||','||chr(39))
I've been racking my brain over this, and I can't figure it out. It seems to me that if "AND OrderNumber IN ('1','2','3')" works, and replace('1,2,3', ',', chr(39)||','||chr(39)) returns '1','2','3', that "AND OrderNumber IN (replace('1,2,3', ',', chr(39)||','||chr(39))" should work.
Any help you might be able to offer on this would be greatly appreciated. I know the rest of the query works. That's why I didn't post it. I'm just stuck on trying to get this IN clause working.
A change to phonetic_man's answer that will allow for NULL elements in the list. The regex format of '[^,]+' for parsing delimited lists does not handle NULL list elements and will return an incorrect value if one exists and thus its use should be avoided. Change the original by deleting the number 2 for instance and see the results. You will get a '3' in the 2nd element's position! Here's a way that handles the NULL and returns the correct value for the element:
SELECT TRIM(REGEXP_SUBSTR(str, '(.*?)(,|$)', 1, LEVEL, NULL, 1)) str
FROM ( SELECT '1,,3,4' str FROM dual )
connect by level <= regexp_count(str, ',') + 1;
See here for more info and proof: https://stackoverflow.com/a/31464699/2543416
Can you try the following query.
SELECT * FROM orders
WHERE orderno IN
(
SELECT TRIM(REGEXP_SUBSTR(str, '[^,]+', 1, LEVEL)) str
FROM ( SELECT '1,2,3,4' str FROM dual )
CONNECT BY INSTR(str, ',', 1, LEVEL - 1) > 0
)
The inline query splitting the string in different rows. So, on executing it you will get the following result.
SELECT trim(regexp_substr(str, '[^,]+', 1, LEVEL)) str
FROM ( SELECT '1,2,3,4' str FROM dual )
CONNECT BY instr(str, ',', 1, LEVEL - 1) > 0
1
2
3
4
Now, passing this result to the main query IN clause should work.
I think the desired clause to be built is:
AND OrderNumber IN (1,2,3)
A numeric list. The example you tested:
AND OrderNumber IN ('1','2','3')
works because an implicit conversion from a VARCHAR2 to a NUMBER is occurring for each element in the list.
The following clause will not work because no implicit conversion of the string '1,2,3' can be made (note the clause has a single string element):
AND OrderNumber IN ('1,2,3')
When doing a replace, you are converting the single string: 1,2,3 with the single string: 1','2','3 and this single string cannot be implicitly converted to a number.

using sys_context to match data string

I'm trying to use data in sys_context form to perform a match in a WHERE clause.
What I put into the context is ('53','89'), which is what is returned when I select against dual.
My where statement is: where to_char(location_id) in sys_context('my_ctx','valoc')
Since I'm not getting the expected response, I'm guessing that what I think Oracle should see is not actually what it sees, but I don't know how to "look" at what's passed to the processor from TOAD.
The original form was where location_id in sys_context('my_ctx','valoc') with (53,89) in valoc, but that didn't return anything either. I'm sensing there may be no answer to my problem.
The problem is that the resulting WHERE clause is equivalent to this:
where to_char(location_id) in '('53','89')'
(didn't double the inner apostrophes for clarity)
The database sees what's retrieved from context as a single value, not as a list of values.
You can use the CONNECT BY trick to achieve your goal:
SELECT 1
FROM dual
WHERE '53' IN ( -- replace '53' with TO_CHAR(location_id)
SELECT regexp_substr('53,89', '[0-9]*', 1, level) -- replace '53,89' with sys_context('my_ctx','valoc')
FROM dual
CONNECT BY regexp_substr('53,89', '[0-9]*', 1, level) IS NOT NULL -- replace '53,89' with sys_context('my_ctx','valoc')
);

PL/SQL query IN comma deliminated string

I am developing an application in Oracle APEX. I have a string with user id's that is comma deliminated which looks like this,
45,4932,20,19
This string is stored as
:P5_USER_ID_LIST
I want a query that will find all users that are within this list my query looks like this
SELECT * FROM users u WHERE u.user_id IN (:P5_USER_ID_LIST);
I keep getting an Oracle error: Invalid number. If I however hard code the string into the query it works. Like this:
SELECT * FROM users u WHERE u.user_id IN (45,4932,20,19);
Anyone know why this might be an issue?
A bind variable binds a value, in this case the string '45,4932,20,19'. You could use dynamic SQL and concatenation as suggested by Randy, but you would need to be very careful that the user is not able to modify this value, otherwise you have a SQL Injection issue.
A safer route would be to put the IDs into an Apex collection in a PL/SQL process:
declare
array apex_application_global.vc_arr2;
begin
array := apex_util.string_to_table (:P5_USER_ID_LIST, ',');
apex_collection.create_or_truncate_collection ('P5_ID_COLL');
apex_collection.add_members ('P5_ID_COLL', array);
end;
Then change your query to:
SELECT * FROM users u WHERE u.user_id IN
(SELECT c001 FROM apex_collections
WHERE collection_name = 'P5_ID_COLL')
An easier solution is to use instr:
SELECT * FROM users u
WHERE instr(',' || :P5_USER_ID_LIST ||',' ,',' || u.user_id|| ',', 1) !=0;
tricks:
',' || :P5_USER_ID_LIST ||','
to make your string ,45,4932,20,19,
',' || u.user_id|| ','
to have i.e. ,32, and avoid to select the 32 being in ,4932,
I have faced this situation several times and here is what i've used:
SELECT *
FROM users u
WHERE ','||to_char(:P5_USER_ID_LIST)||',' like '%,'||to_char(u.user_id)||',%'
ive used the like operator but you must be a little carefull of one aspect here: your item P5_USER_ID_LIST must be ",45,4932,20,19," so that like will compare with an exact number "',45,'".
When using it like this, the select will not mistake lets say : 5 with 15, 155, 55.
Try it out and let me know how it goes;)
Cheers ,
Alex
Create a native query rather than using "createQuery/createNamedQuery"
The reason this is an issue is that you cannot just bind an in list the way you want, and just about everyone makes this mistake at least once as they are learning Oracle (and probably SQL!).
When you bind the string '32,64,128', it effectively becomes a query like:
select ...
from t
where t.c1 in ('32,64,128')
To Oracle this is totally different to:
select ...
from t
where t.c1 in (32,64,128)
The first example has a single string value in the in list and the second has a 3 numbers in the in list. The reason you get an invalid number error is because Oracle attempts to cast the string '32,64,128' into a number, which it cannot do due to the commas in the string.
A variation of this "how do I bind an in list" question has come up on here quite a few times recently.
Generically, and without resorting to any PLSQL, worrying about SQL Injection or not binding the query correctly, you can use this trick:
with bound_inlist
as
(
select
substr(txt,
instr (txt, ',', 1, level ) + 1,
instr (txt, ',', 1, level+1) - instr (txt, ',', 1, level) -1 )
as token
from (select ','||:txt||',' txt from dual)
connect by level <= length(:txt)-length(replace(:txt,',',''))+1
)
select *
from bound_inlist a, users u
where a.token = u.id;
If possible the best idea may be to not store your user ids in csv! Put them in a table or failing that an array etc. You cannot bind a csv field as a number.
Please dont use: WHERE ','||to_char(:P5_USER_ID_LIST)||',' like '%,'||to_char(u.user_id)||',%' because you'll force a full table scan although with the users table you may not have that many so the impact will be low but against other tables in an enterprise environment this is a problem.
EDIT: I have put together a script to demonstrate the differences between the regex method and the wildcard like method. Not only is regex faster but it's also a lot more robust.
-- Create table
create table CSV_TEST
(
NUM NUMBER not null,
STR VARCHAR2(20)
);
create sequence csv_test_seq;
begin
for j in 1..10 loop
for i in 1..500000 loop
insert into csv_test( num, str ) values ( csv_test_seq.nextval, to_char( csv_test_seq.nextval ));
end loop;
commit;
end loop;
end;
/
-- Create/Recreate primary, unique and foreign key constraints
alter table CSV_TEST
add constraint CSV_TEST_PK primary key (NUM)
using index ;
alter table CSV_TEST
add constraint CSV_TEST_FK unique (STR)
using index;
select sysdate from dual;
select *
from csv_test t
where t.num in ( Select Regexp_Substr('100001, 100002, 100003 , 100004, 100005','[^,]+', 1, Level) From Dual
Connect By Regexp_Substr('100001, 100002,100003, 100004, 100005', '[^,]+', 1, Level) Is Not Null);
select sysdate from dual;
select *
from csv_test t
where ('%,' || '100001,100002, 100003, 100004 ,100005' || ',%') like '%,' || num || ',%';
select sysdate from dual;
select *
from csv_test t
where t.num in ( Select Regexp_Substr('100001, 100002, 100003 , 100004, 100005','[^,]+', 1, Level) From Dual
Connect By Regexp_Substr('100001, 100002,100003, 100004, 100005', '[^,]+', 1, Level) Is Not Null);
select sysdate from dual;
select *
from csv_test t
where ('%,' || '100001,100002, 100003, 100004 ,100005' || ',%') like '%,' || num || ',%';
select sysdate from dual;
drop table csv_test;
drop sequence csv_test_seq;
Solution from Tony Andrews works for me. The process should be added to "Page processing" >> "After submit">> "Processes".
As you are Storing User Ids as String so You can Easily match String Using Like as Below
SELECT * FROM users u WHERE u.user_id LIKE '%'||(:P5_USER_ID_LIST)||'%'
For Example
:P5_USER_ID_LIST = 45,4932,20,19
Your Query Surely Will return Any of 1 User Id which Matches to Users table
This Will Surely Resolve Your Issue , Enjoy
you will need to run this as dynamic SQL.
create the entire string, then run it dynamically.

Resources