Flag duplicate characters in a string - oracle

I'm using oracle apex and i'm trying to write a pl/sql statement that will flag duplicates in a string. For example the string 'P,T,P,C' has two occurrences of the letter 'P' so it should raise an error. After all my digging the closest I got to achieving this was by using REGEXP_LIKE, but my regular expression skills are sub par. If anyone can assist that would be much appreciated.
DECLARE
v_seccode varchar2(10) := 'P,T,P,C';
BEGIN
if regexp_like(v_seccode, '(P{2,}?|C{2,}?|W{2,}?|T{2,}?)') then
raise_application_error(-20001,'You can not have the same SEC code listed more than once!');
end if;
END;

If your apex version is relatively recent you have access to the APEX_STRING API. Here is some code using that API, using same logic #Littlefoot showed in the other answer:
DECLARE
l_arr1 apex_t_varchar2;
l_arr2 apex_t_varchar2;
BEGIN
-- convert string to array with "," delimiter
l_arr1 := apex_string.split(:P1_SECCODE,',');
l_arr2 := l_arr1;
-- remove dupes from l_arr2
l_arr2 := l_arr2 MULTISET UNION DISTINCT l_arr2;
IF l_arr2 != l_arr1 THEN
return 'You can not have the same SEC code listed more than once!';
END IF;
END;
/

As you're using Apex, I suggest you create a validation within it. Code you posted suggests that you'd want to handle that elsewhere (process? Database trigger?).
Presuming that page item name is P1_SECCODE, validation - a PL/SQL function that returns error text - might look like this (read comments within code):
declare
l_seccode varchar2(20);
begin
-- Split P1_SECCODE into rows, fetch distinct values and aggregate them back.
-- If P1_SECCODE = 'P,T,P,C' then the L_SECCODE = 'P,T,C'
select listagg(distinct regexp_substr(:P1_SECCODE, '[^,]+', 1, level), ',')
within group (order by null)
into l_seccode
from dual
connect by level <= regexp_count(:P1_SECCODE, ',') + 1;
-- Now compare whether P1_SECCODE has the same number of commas as L_SECCODE.
-- If not, it means that P1_SECCODE contained duplicates so - return an error message
if regexp_count(:P1_SECCODE, ',') <> regexp_count(l_seccode, ',') then
return 'You can not have the same SEC code listed more than once!';
end if;
end;
If your database version doesn't support distinct within listagg, then first fetch distinct values and then aggregate them:
declare
l_seccode varchar2(20);
begin
-- Split P1_SECCODE into rows, fetch distinct values and aggregate them back.
-- If P1_SECCODE = 'P,T,P,C' then the L_SECCODE = 'P,T,C'
with temp as
(select distinct regexp_substr(:P1_SECCODE, '[^,]+', 1, level), ',') val
from dual
connect by level <= regexp_count(:P1_SECCODE, ',') + 1
)
select listagg(val, ',') within group (order by null)
into l_seccode
from temp;
-- Now compare whether P1_SECCODE has the same number of commas as L_SECCODE.
-- If not, it means that P1_SECCODE contained duplicates so - return an error message
if regexp_count(:P1_SECCODE, ',') <> regexp_count(l_seccode, ',') then
return 'You can not have the same SEC code listed more than once!';
end if;
end;

Given that your string can only have 4 possible letters PCWT, you could just use LIKE expressions along with a regular query:
SELECT *
FROM yourTable
WHERE string NOT LIKE '%P%P%' AND
string NOT LIKE '%C%C%' AND
string NOT LIKE '%W%W%' AND
string NOT LIKE '%T%T%';

Related

Search string to match Data inside of all existing columns and rows from Views in Oracle SQL

The code below is looking for a string to match a column name. However I would like to search for a string to match Data (meaning, search on each existing column and row from all views - not column names). I want the results to show me all Views Names that contain that string on their Data. Hope this makes sense.
begin
dbms_output.put_line('Owner View name');
dbms_output.put_line('------------------------------ -------------------------------');
for r in (
select v.owner, v.view_name, v.text
from all_views v
where v.owner <> 'SYS'
)
loop
if lower(r.text) like '%my_String%' then
dbms_output.put_line(rpad(r.owner,31) || r.view_name);
end if;
end loop;
end;
I suggest you at least review some of the Oracle documentation. Your statement "Oracle SQL... i am using Oracle SQL Developer" indicates a lack of understanding. So lets clear some of that up. You are actually using 3 separate software tools:
Oracle SQL - all access to data being stored in your Oracle database.
Oracle Pl/SQL - the programming language, which tightly integrates with but is still separate from SQL. This is the language in which your script is written.
Oracle SQL Developer - an IDE within which you develop, run, etc run your Pl/SQL scripts and/or SQL statements.
Now as for your script. The syntax is fine and it will run, successfully retrieving all non sys owned views and printing the names of those containing the specified search string. However, as it currently stands it'll never find a match. Your match statement attempts to match a lower case string to a string that contains the character 'S' (upper case). There will never be a match. Also, keep in mind that within LIKE the underscore (_) is a single character wild card. You may need to escape it as "like '%my_ ...'". With these in mind we come to:
REVISED
The requirement to actually find the string in view columns completely changes things from your original query. Although the title does suggest that was actually the initial desire. What you want to accomplish is much more complex and cannot be done in SQL alone; it requires PL/SQL or an external language code. The reason is that you need run select but you don't know against what nor what columns nor even how many columns area needed. For that you need to identify the view, isolate the viable columns and generate the appropriate SQL dynamically then actually execute that sql and check the results.
Two approaches come to mind: Parse the view to extract the columns (something I would even consider doing in PL/SQL) or join ALL_VIEWS with ALL_TAB_COLUMNS (All table columns). That we'll do. The majority of the code will be constructing the SQL and the necessary error handling dynamic SQL essentially forces on you. I've created this a a procedure with 2 parameters: the target string, and the schema.
create or replace procedure find_string_in_view(
schema_name_in varchar2
, target_string_in varchar2
)
is
--- set up components for eventual execution
k_new_line constant varchar2(2) := chr(10);
k_base_statement constant varchar2(50) :=
q'!select count(*) from <view_name> where 1=1 and !' || k_new_line;
k_where_statement constant varchar2(50) :=
q'! <column_name> like '%<target>%' !' || k_new_line;
k_limit_statement constant varchar2(20) :=
' ) and rownum < 2';
k_max_allowed_errors constant integer := 3;
--- cursor for views and column names
cursor c_view_columns is
(select av.owner,av.view_name , atc.column_name
, lead(atc.column_name) over (partition by av.view_name order by atc.column_id) next_col
, lag(atc.column_name) over (partition by av.view_name order by atc.column_id) prev_col
from all_views av
join all_tab_columns atc
on ( atc.owner = av.owner
and atc.table_name = av.view_name
)
where av.owner = upper(schema_name_in)
and atc.data_type in
('CHAR', 'NCHAR', 'VARCHAR2','NVARCHAR2','VARCHAR','NVARCHAR')
) ;
--- local variables
m_current_view varchar2(61);
m_sql_errors integer := 0;
m_where_connector varchar(2);
m_sql_statement varchar2(4000);
-- local helper function
function view_has_string
return boolean
is
l_item_count integer := 0;
begin
execute immediate m_sql_statement into l_item_count;
return (l_item_count > 0);
exception
when others then
dbms_output.put_line(rpad('-',61,'-') || k_new_line);
dbms_output.put_line('Error processing:: ' || m_current_view);
dbms_output.put_line('Statement::' || k_new_line || m_sql_statement);
dbms_output.put_line(sqlerrm);
m_sql_errors := m_sql_errors + 1;
if m_sql_errors >= k_max_allowed_errors
then
raise_application_error(-20199,'Maximun errors allowed reach. Terminating');
end if;
return false;
end view_has_string;
begin -- MAIN --
dbms_output.put_line('Owner View name');
dbms_output.put_line('------------------------------ -------------------------------');
for rec in c_view_columns
loop
if rec.prev_col is null
then
m_current_view := rec.owner || '.' || rec.view_name;
m_sql_statement := replace(k_base_statement,'<view_name>',m_current_view);
m_where_connector := ' (';
end if;
m_sql_statement := m_sql_statement || m_where_connector
|| replace(k_where_statement,'<column_name>',rec.column_name);
m_where_connector := 'or' ;
if rec.next_col is null
then
m_sql_statement := replace(m_sql_statement,'<target>',target_string_in);
m_sql_statement := m_sql_statement || k_limit_statement;
if view_has_string
then
dbms_output.put_line(rpad(rec.owner,31) || rec.view_name);
end if;
end if;
end loop;
end find_string_in_view;
select v.owner, v.view_name, v.text
from all_views v
where v.owner <> 'SYS' and lower(v.text) like '%my_String%'
one SQL do this?

Oracle single row into variable

I need to get all result in one row... it is working but when i want to see it in dbms there is nothink.. why ?
CREATE OR REPLACE PROCEDURE NXMESEP.SP_IN_CHECK_AND_SEND_SMS
( RC_TABLE0 OUT SYS_REFCURSOR,
RS_CODE OUT VARCHAR2, -- RETURN 코드
RS_MSG OUT VARCHAR2
) IS ERROR_EXCEPTION EXCEPTION;
BEGIN
begin
DECLARE
promena varchar2(32767);
BEGIN
OPEN RC_TABLE0 FOR
SELECT listagg(ITEM_ID,', ') within group(order by ITEM_ID)
INTO promena
FROM TB_PL_M_WRKORD WRKOD
WHERE 1 = 1
AND WO_DATE = '20181012'
AND WRKOD.ITEM_ID NOT IN (SELECT ITEM_ID FROM TB_CM_M_FERT_COST_CHK FERT)
AND WC_ID = 'U';
LOOP
FETCH rc_table0 INTO promena;
EXIT WHEN rc_table0%NOTFOUND;
dbms_output.put_line(promena);
END LOOP;
CLOSE rc_table0;
end;
EXCEPTION
.... END;
RS_CODE := 'S'; RS_MSG := 'Complete successfully!';
RETURN; END SP_CHECK_AND_SEND_SMS; /
This should be promena that i expected..
" 12993NXUA, 13595NXUA, 14495NXUA, 16589NX, 16589NX, 16590NX, 16590NX, 16622NX, 16622NX "
Now it is working but im getting unknown error ORA-65535 every time when i execute. But after this I can see dbms result is ok.
Assuming your real code has RC_TABLE0 declared, as a ref cursor, then your variable ends up null because opening the cursor into something doesn't really do anything. You can't open a cursor and select something from the cursor query into a separate variable at the same time, whichever way round you try to do it. You need either a cursor, or a simple select ... into:
DECLARE
promena varchar2(32767);
BEGIN
SELECT listagg(ITEM_ID,', ') within group (order by ITEM_ID)
INTO promena
FROM TB_PL_M_WRKORD WRKOD
WHERE 1 = 1
AND WO_DATE = '20181012'
AND WRKOD.ITEM_ID NOT IN (SELECT ITEM_ID FROM TB_CM_M_FERT_COST_CHK FERT)
AND WC_ID = 'U';
dbms_output.put_line('test: '||promena);
END;
/
test: 12993NXUA, 13595NXUA, 14495NXUA ...
PL/SQL procedure successfully completed.
You also have to set serveroutput on or equivalent to actually see the results, of course.
I've also removed the redundant distinct, the unnecessary select .. from dual - which seemed to be part of the odd cursor construct - and the extra level of begin/end.
Incidentally, your code implies that wo_date is a string, which seems unlikely, or at least not ideal. If it is actually a real date then you should not be using a string for the comparison as you're forcing implicit conversions; use an actual date instead, maybe as an ANSI date literal:
AND WO_DATE = DATE '2018-10-12'
If you did really want to use an explicit cursor approach you would need to use a loop to populate the string variable:
DECLARE
promena varchar2(32767);
rc_table0 sys_refcursor;
BEGIN
OPEN rc_table0 FOR
SELECT DISTINCT listagg(ITEM_ID,', ') within group (order by ITEM_ID)
FROM TB_PL_M_WRKORD WRKOD
WHERE 1 = 1
AND WO_DATE = '20181012'
AND WRKOD.ITEM_ID NOT IN (SELECT ITEM_ID FROM TB_CM_M_FERT_COST_CHK FERT)
AND WC_ID = 'U';
LOOP
FETCH rc_table0 INTO promena;
EXIT WHEN rc_table0%NOTFOUND;
dbms_output.put_line('test: '||promena);
END LOOP;
CLOSE rc_table0;
END;
/
As you're only expecting a single row back there isn't much point doing that; and if you expected multiple rows (from a modified query, e.g. getting several days data and grouping by day) then an implicit cursor would be simpler anyway:
BEGIN
FOR r IN (
SELECT DISTINCT listagg(ITEM_ID,', ') within group (order by ITEM_ID) AS promena
FROM TB_PL_M_WRKORD WRKOD
WHERE 1 = 1
AND WO_DATE = '20181012'
AND WRKOD.ITEM_ID NOT IN (SELECT ITEM_ID FROM TB_CM_M_FERT_COST_CHK FERT)
AND WC_ID = 'U'
)
LOOP
dbms_output.put_line('test: '||r.promena);
END LOOP;
END;
/
If this is really part of a procedure and the rc_table0 is an OUT parameter then you just can't do this. In code you posted as an answer you tried:
OPEN RC_TABLE0 FOR
SELECT listagg(ITEM_ID,', ') within group(order by ITEM_ID)
INTO promena
FROM TB_PL_M_WRKORD WRKOD
...
In that construct the into is still ignored, because the open doesn't fetch anything. And if you loop and fetch inside your procedure to display the results as I did above then you are consuming the result set, so the caller will get no results (or "ORA-01001: invalid cursor" if you close it inside the procedure).
You just can't do both, unless you re-open the cursor, which seems like overhead you probably don't want...

Creating SQL-Injection proof dynamic where-clause from collection in PL/SQL

I need to execute a query where the where-clause is generated based on user input. The input consists of 0 or more pairs of varchar2s.
For example:
[('firstname','John')
,('lastname','Smith')
,('street','somestreetname')]
This would translate into:
where (attrib = 'firstname' and value = 'John')
and (attrib = 'lastname' and value = 'Smith')
and (attrib = 'street' and value = 'somestreetname')
This is not the actual data structure as there are several tables but for this example lets keep it simple and say the values are in 1 table. Also I know the parentheses are not necessary in this case but I put them there to make things clear.
What I do now is loop over them and concatinate them to the SQL string. I made a stored proc to generate this where-clause which might also not be very secure since I just concat to the original query.
Something like the following, where I try to get the ID's of the nodes that correspond with the requested parameters:
l_query := select DISTINCT n.id from node n, attribute_values av
where av.node_id = n.id ' || getWhereClause(p_params)
open l_rc
for l_query;
fetch l_rc bulk collect into l_Ids;
close l_rc;
But this is not secure so I'm looking for a way that can guaranty security and prevent SQL-Injection attacks from happening.
Does anyone have any idea on how this is done in a secure way? I would like to use bindings but I don't see how I can do this when you dont know the number of parameters.
DB: v12.1.0.2 (i think)
It's still a bit unclear and generalised, but assuming you have a schema-level collection type, something like:
create type t_attr_value_pair as object (attrib varchar2(30), value varchar2(30))
/
create type t_attr_value_pairs as table of t_attr_value_pair
/
then you can use the attribute/value pairs in the collection for the bind:
declare
l_query varchar2(4000);
l_rc sys_refcursor;
type t_ids is table of number;
l_ids t_ids;
l_attr_value_pairs t_attr_value_pairs;
-- this is as shown in the question; sounds like it isn't exactly how you have it
p_params varchar2(4000) := q'^[('firstname','John')
,('lastname','Smith')
,('street','somestreetname')]^';
begin
-- whatever mechanism you want to get the value pairs into a collection;
-- this is just a quick hack to translate your example string
select t_attr_value_pair(rtrim(ltrim(
regexp_substr(replace(p_params, chr(10)), '(.*?)(,|$)', 1, (2 * level) - 1, null, 1),
'[('''), ''''),
rtrim(ltrim(
regexp_substr(replace(p_params, chr(10)), '(.*?)(,|$)', 1, 2 * level, null, 1),
''''), ''')]'))
bulk collect into l_attr_value_pairs
from dual
connect by level <= regexp_count(p_params, ',') / 2 + 1;
l_query := 'select DISTINCT id from attribute_values
where (attrib, value) in ((select attrib, value from table(:a)))';
open l_rc for l_query using l_attr_value_pairs;
fetch l_rc bulk collect into l_ids;
close l_rc;
for i in 1..l_ids.count loop
dbms_output.put_line('id ' || l_ids(i));
end loop;
end;
/
although it doesn't need to be dynamic with this approach:
...
begin
-- whatever mechamism you want to get the value pairs into a collection
...
select DISTINCT id
bulk collect into l_ids
from attribute_values
where (attrib, value) in ((select attrib, value from table(l_attr_value_pairs)));
for i in 1..l_ids.count loop
dbms_output.put_line('id ' || l_ids(i));
end loop;
end;
/
or with a join to the table collection expression:
select DISTINCT av.id
bulk collect into l_ids
from table(l_attr_value_pairs) t
join attribute_values av on av.attrib = t.attrib and av.value = t.value;
Other collection types will need different approaches.
Alternatively, you could still build up your where clause with one condition per attribute/value pair, while still making them bind variables - but you would need two levels of dynamic SQL, similar to this.

Alternative to REGEXP_LIKE due to regular expression too long

I am getting an ORA-12733: regular expression too long error when trying to find if certain ids already inside the database.
regexp_like (','||a.IDs||',',',('||replace(b.IDs,',','|')||'),')
a.IDs and b.IDs are in a format of something like id=16069,16070,16071,16072,16099,16100.
i will replace comma with | in b so it will tell me if any of the number is matched. The length of both a.IDs and b.IDs might vary from different queries.
Oracle regexp_like limit is only 512. Anyone know if other possible solutions?
Why on earth do you store list of numbers as string?
Anyway, one possible solution is this one. Create TYPE and FUNCTION like this:
CREATE OR REPLACE TYPE NUMBER_TABLE_TYPE AS TABLE OF NUMBER;
CREATE OR REPLACE FUNCTION SplitArray(LIST IN VARCHAR2, Separator IN VARCHAR2) RETURN NUMBER_TABLE_TYPE IS
OutTable NUMBER_TABLE_TYPE;
BEGIN
IF LIST IS NULL THEN
RETURN NULL;
ELSE
SELECT REGEXP_SUBSTR(LIST, '[^'||Separator||']+', 1, LEVEL)
BULK COLLECT INTO OutTable
FROM dual
CONNECT BY REGEXP_SUBSTR(LIST, '[^'||Separator||']+', 1, LEVEL) IS NOT NULL;
END IF;
IF OutTable.COUNT > 0 THEN
RETURN OutTable;
ELSE
RETURN NULL;
END IF;
END SplitArray;
Then you query for a single number as this:
WHERE 16071 MEMBER OF SplitArray(a.IDs, ',')
or for several numbers as this:
WHERE SplitArray(b.IDs, ',') SUBMULTISET OF SplitArray(a.IDs, ',')
Have a look at Multiset Conditions

How we can get the value from string: 1;BWQTY;4|2;NGRID;34664 and store into a variable if any specific SUBSTRING found?

I have a table name as event_creation_template which having column as TEXT ,which contains value like below :
1;BWQTY;4|2;NGRID;34664
1;DTYPE;Amount|2;LOYAL;R
How we can get the value from string:
1;BWQTY;4|2;NGRID;34664
If string is having BWQTY then store 4 into variable again search for the NGRID ,if string contains NGRID , we need in 34664 into different variable.
using Oracle PL/SQL.
considering the string like 'BWQTY' will always lie between two non-aphanumeric characters and the corresponding values like 4 lies between either non-alphanumeric character or appears at the end like in the two examples above you can use below code
CREATE OR REPLACE PR_PREP
IS
V_VAR1 VARCHAR2(100);
V_-VAR2 VARCHAR2(100);
V_SEARCH_VAL1 VARCHAR2(100) := 'BWQTY';
V_SEARCH_VAL2 VARCHAR2(100) := 'NGRID';
CURSOR C1
IS
SELECT TEXT FROM EVENT_CREATION_TEMPLATE;
BEGIN
FOR REC IN C1
LOOP
IF INSTR(REC.TEXT,V_SEARCH_VAL1) > 0 THEN
SELECT NVL(SUBSTR(SUBSTR(REC.TEXT,INSTR(REC.TEXT,V_SEARCH_VAL1)+LENGTH(V_SEARCH_VAL1)+1), 1, REGEXP_INSTR( SUBSTR(REC.TEXT,INSTR(REC.TEXT,V_SEARCH_VAL1)+LENGTH(V_SEARCH_VAL1)+1), '[^0-9a-zA-Z]',1)-1), SUBSTR(REC.TEXT,INSTR(REC.TEXT,V_SEARCH_VAL1)+LENGTH(V_SEARCH_VAL1)+1))
INTO V_VAR1
FROM DUAL ;
END IF;
IF INSTR(REC.TEXT,V_SEARCH_VAL2) > 0 THEN
SELECT NVL(SUBSTR(SUBSTR(REC.TEXT,INSTR(REC.TEXT,V_SEARCH_VAL2)+LENGTH(V_SEARCH_VAL2)+1), 1, REGEXP_INSTR( SUBSTR(REC.TEXT,INSTR(REC.TEXT,V_SEARCH_VAL2)+LENGTH(V_SEARCH_VAL2)+1), '[^0-9a-zA-Z]',1)-1), SUBSTR(REC.TEXT,INSTR(REC.TEXT,V_SEARCH_VAL2)+LENGTH(V_SEARCH_VAL2)+1))
INTO V_VAR2
FROM DUAL ;
END IF;
-- DO FURTHER WORK
END LOOP;
END;
You can use regular expressions
for BWQTY:
select str,
SUBSTR(REGEXP_SUBSTR(str, '([BWQTY]{5})([;])([0-9]+)'),
INSTR(REGEXP_SUBSTR(str, '([BWQTY]{5})([;])([0-9]+)'), ';') + 1 ,
LENGTH(REGEXP_SUBSTR(str, '([BWQTY]{5})([;])([0-9]+)')) - INSTR(REGEXP_SUBSTR(str, '([BWQTY]{5})([;])([0-9]+)'), ';')) AS VALUE
from test
for NGRID:
select str,
SUBSTR(REGEXP_SUBSTR(str, '([NGRID]{5})([;])([0-9]+)'),
INSTR(REGEXP_SUBSTR(str, '([NGRID]{5})([;])([0-9]+)'), ';') + 1 ,
LENGTH(REGEXP_SUBSTR(str, '([NGRID]{5})([;])([0-9]+)')) - INSTR(REGEXP_SUBSTR(str, '([NGRID]{5})([;])([0-9]+)'), ';')) AS VALUE
from test
Where str is your input string...

Resources