Scalar function throws error while using in SQL - sql

My question is:
Write a query to display user name and password. Password should be generated by concatenating first two characters of user name , length of the user name and last three numbers in the phone number and give an alias name as USER_PASSWORD. Sort the results based on the user name in descending order.
select
name,
concat(substring(name, 1, 2), cast(len(name) as varchar), cast(right(phno, 3) as varchar)) as USER_PASSWORD
from
users
order by
name desc;
I get this error:
cast(len(name) as varchar),
ERROR at line 5: ORA-00906: missing left parenthesis
Thanks

You have five issues:
CONCAT only takes two arguments so you either need CONCAT(a, CONCAT(b, c)) or use the || string concatenation operator a || b || c
CAST requires the data type and length CAST(a AS VARCHAR2(10))
SUBSTRING is not an Oracle function, you want SUBSTR;
LEN is not an Oracle function, you want LENGTH;
RIGHT is not an Oracle function, your want SUBSTR with a negative index.
SELECT name,
concat(
substr(name, 1, 2),
concat(
cast(length(name) as varchar2(10)),
cast(SUBSTR(phno, -3) as varchar2(10))
)
) as USER_PASSWORD
from users
order by name desc;
However, you do not need to explicitly use CAST as you can use an implicit conversion between data types:
SELECT name,
substr(name, 1, 2) || length(name) || SUBSTR(phno, -3) as USER_PASSWORD
from users
order by name desc;
Which, for the sample data:
CREATE TABLE users (name, phno) AS
SELECT 'Benny', '0123111' FROM DUAL UNION ALL
SELECT 'Betty', '4567111' FROM DUAL UNION ALL
SELECT 'Beryl', '2222111' FROM DUAL;
Both output:
NAME
USER_PASSWORD
Betty
Be5111
Beryl
Be5111
Benny
Be5111
fiddle
Which leads to the final point, don't generate obvious passwords; generate random or pseudo-random passwords. Then don't store them as plain text; instead store them as a salted-hash.

Concat() is limited to two arguments in Oracle. Use || instead.
with my_data as (
select 'abcdefg' as name, 12345 as phno from dual
)
select
name,
substr(name, 1, 2) ||
length(name) ||
substr(to_char(phno),-3) as user_password
from my_data
| NAME | USER_PASSWORD |
| --------|---------------|
| abcdefg | ab7345 |
fiddle

Related

Oracle regular expression match string from last occurence

I'm still learning regexp in oracle and stuck with below error. Below is my sample code
SELECT DISTINCT COALESCE(TO_NUMBER(regexp_substr(USERNAME, '[^.]+', 1, 2)), ID) ID ,
COALESCE(regexp_substr(USERNAME, '[^.]+', 1, 1), USERNAME) AS USERNAME
FROM logs;
ORA-01722: invalid number
01722. 00000 - "invalid number"
*Cause: The specified number was invalid.
*Action: Specify a valid number.
Table Data
Username ID
Ravi.1234 1234
Krishna.12345 12345
Ravi.Krishna.1234567 1234567
R.Krishna.987 987
Ravi.K.567890 567890
R.Krish 123
Ravi 456
Expected Output
ID Username
1234 Ravi
12345 Krishna
1234567 Ravi.Krishna
987 R.Krishna
567890 Ravi.K
How to reframe the query to get the output needed. Can substr be used instead of regexp will it give desired output? This is used in oracle database not in sql. Thanks in advance.
If I understood your assignment correctly (see my comments under your question), here is how you can do this with standard string functions and conditions:
with
table_data (username, id) as (
select 'Ravi.1234' , '1234' from dual union all
select 'Krishna.12345' , '12345' from dual union all
select 'Ravi.Krishna.1234567', '1234567' from dual union all
select 'R.Krishna.987' , '987' from dual union all
select 'Ravi.K.567890' , '567890' from dual union all
select 'R.Krish' , '123' from dual union all
select 'Ravi' , '456' from dual
)
select id, substr(username, 1, instr(username, '.', -1) - 1) as username
from table_data
where username like '%.' || id
;
ID USERNAME
------- --------------------
1234 Ravi
12345 Krishna
1234567 Ravi.Krishna
987 R.Krishna
567890 Ravi.K
In the LIKE condition in the WHERE clause, % is a wildcard for "any string of any length, including zero"; that must be followed by a literal dot and then by the ID, and that must be the whole USERNAME string. In select, instr(username, '.', -1) finds the position of the "first" dot in username, but counting from the end and moving left - that is what the minus sign means.
With regular expression functions and conditions:
select id, regexp_substr(username, '^(.*)\.' || id || '$', 1, 1, null, 1) as username
from table_data
where regexp_like(username, '\.' || id || '$')
;
The sixth argument to regexp_substr means "the first substring enclosed in parentheses" (first "capture group" is the technical term).
I think REGEXP_REPLACE() would suit well for your case while filtering out the values having at least one digit. In the current case, you're trying to convert the second portions of the Username strings to number, but not all of them are numeric, the error raises due to this. Moreover, you can also extract the ID column from Username column. e.g. no need to hold seperate ID column within your original table.
Thus, consider using
SELECT TO_NUMBER( REGEXP_REPLACE(Username, '[^0-9]+') ) AS ID,
RTRIM( REGEXP_REPLACE(Username, '[^.]+$'),'.') AS "Username"
FROM logs
WHERE REGEXP_LIKE(Username,'[0-9]')
the following option would be an alternative to above one by using less Regular expression
SELECT TO_NUMBER( SUBSTR( Username, INSTR(Username, '.',-1)+1, LENGTH( Username ) )) AS ID,
SUBSTR( Username, 1, INSTR(Username, '.',-1)-1 ) AS "Username"
FROM logs
WHERE REGEXP_LIKE(Username,'[0-9]')
Demo

How to select the list of words containing a particular substring as part of a SQL query (oracle)?

I'm trying to return the list of "words" (separated by spaces) containing a certain substring within a string as part of an Oracle Sql query. Would like to return the result as a comma separated list. Separate rows for each match would also work.
Example String in [text_col] field:
some words 123-asdf-789A and also this one 456-asdf-555A more words etc.
Desired result: 123-asdf-789A, 456-asdf-555A
This is what I have so far but it only returns the first result and the fact that it's two separate regular expressions makes it difficult to concatenate all matches as I would like to do.
CONCAT(REGEXP_SUBSTR(text_col, ''(([^[:space:]]+)\asdf)'', 1, 1, ''i'', 1),
REGEXP_SUBSTR(text_col, ''\asdf([^[:space:]]+)'', 1, 1, ''i'', 1))
You can use some regexp functions together as :
with tab(str) as
(
select 'some words 123-asdf-789A and also this one 456-asdf-555A more words etc' from dual
), t as
(
select regexp_substr(str,'[^[:space:]]+',1,level) as str, level as lvl
from tab
connect by level <= regexp_count(str,'[:space:]')
)
select listagg(str,',') within group (order by lvl) as "Result"
from t
where regexp_like(str,'-');
Result
---------------------------------
123-asdf-789A,456-asdf-555A
Demo
first split by spaces (through [:space:] posix) and take the ones containing dash characters, and finally concatenate by listagg() function
Use a recursive sub-query factoring clause and iterate through all the matches concatenating the string as you go:
Oracle Setup:
CREATE TABLE test_data ( value ) AS
SELECT 'some words 123-asdf-789A and also this one 456-asdf-555A more words etc.' FROM DUAL UNION ALL
SELECT 'some words without the expected sub-string' FROM DUAL UNION ALL
SELECT 'asdf asdf-123 456-asdf 78-asdf-90' FROM DUAL
Query:
WITH matches ( value, idx, cnt, match ) AS (
SELECT value,
0,
REGEXP_COUNT( value, '\S*asdf\S*' ),
CAST( NULL AS VARCHAR2(4000) )
FROM test_data
UNION ALL
SELECT value,
idx + 1,
cnt,
CASE idx WHEN 0 THEN '' ELSE match || ' ' END
|| REGEXP_SUBSTR( value, '\S*asdf\S*', 1, idx + 1 )
FROM matches
WHERE idx < cnt
)
SELECT value, match
FROM matches
WHERE idx = cnt;
Output:
VALUE | MATCH
:----------------------------------------------------------------------- | :--------------------------------
some words without the expected sub-string | null
some words 123-asdf-789A and also this one 456-asdf-555A more words etc. | 123-asdf-789A 456-asdf-555A
asdf asdf-123 456-asdf 78-asdf-90 | asdf asdf-123 456-asdf 78-asdf-90
db<>fiddle here

Escaping characters for use with Oracle's Xmltable

I'm using Xmltable to convert a field of comma-delimited email addresses to a table of values.
WITH
data AS
(
select 1 ID, 'foo&bar#domain.tld,bar#domain.tld' recipients from dual
)
select ID, trim(COLUMN_VALUE) recipient
from data,xmltable(('"'|| REPLACE( recipients , ',', '","') || '"'))
produces an error:
[72000][19112] ORA-19112: error raised during evaluation: XVM-01003:
[XPST0003] Syntax error at '"foo' 1
"foo&bar#domain.tld","bar#domain.tld" - ^
However, when I replace the & with its entity value (&):
WITH
DATA AS
(
select 1 ID, 'foo&bar#domain.tld,bar#domain.tld' recipients from dual
)
select ID
-- & --> &
, replace( trim(COLUMN_VALUE), '&', '&') recipient
from data
-- & --> &
,xmltable(('"'|| REPLACE( replace( recipients, '&','&') , ',', '","') || '"'))
the query works:
ID,RECIPIENT
1,foo&bar#domain.tld
1,bar#domain.tld
I'm imaging that there might be other characters that are valid in an email address, but will be problematic for Xmltable.
Is there a better way to do this?
You could use the built-in dbms_xmlgen.convert() function:
with data (id, recipients) as
(
select 1, 'foo&bar#domain.tld,bar#domain.tld' from dual
)
select d.id, dbms_xmlgen.convert(x.column_value.getstringval(), 1) as recipient
from data d
cross join
xmltable(('"' || replace(dbms_xmlgen.convert(d.recipients, 0), ',', '","') || '"')) x
ID RECIPIENT
---------- ------------------------------
1 foo&bar#domain.tld
1 bar#domain.tld
The inner call dbms_xmlgen.convert(d.recipients, 0) gives you
foo&bar#domain.tld,bar#domain.tld
After that has been modified to have double quotes around each comma-separated value and been split into multiple rows, you end up with column_value as:
foo&bar#domain.tld
bar#domain.tld
so the outer dbms_xmlgen.convert(x.column_value.getstringval(), 1) converts any encoded entities back to their plain versions.
If you do this in a PL/SQL context then you can use dbms_xmlgen.entity_encode and dbms_xmlgen.entity_decode instead of the fixed 0 and 1, but those aren't available in plain SQL.
(There are only five entities to worry about anyway, but you still might as well allow for them all - whether they are valid in email addresses or not - and using a function call like this is maybe going to be less confusing to future maintainers...)

How Select SUBSTRING in ORACLE

I need select a subtring to get the consecutive number from a table field. My table is:
ORDER_NUM ORDER_DATE ORDER_TYPE LOCATION SALE_TYPE
10501702315618 08/01/17 43223 1050 18
105017023186230 21/01/17 43221 1050 230
The field ORDER_NUM is generated as follows
[LOCATION] + [YY] + [CONSECUTIVE_NUMBER] + [SALE_TYPE]
The length of the fields LOCATION and SALE_TYPE Can be different.So, my query is:
SELECT
SUBSTR(ORDER_NUM,LENGTH (ORDER_TYPE) + 3,LENGTH (ORDER_NUM) - LENGTH ( SALE_TYPE ) ),
ORDER_DATE
FROM
CAT_ORDERS
WHERE
LOCATION = '1050'
AND SALE_TYPE = '18'
The result is
SELECT SUBSTR('10501702315618',7,12) from dual
RESULT: 02315618
Where the index position is:
12345678901234
10501702315618
How i can delete the SALE_TYPE characters from the string? There is another function for this ?
Thank's!
Your description refers to the location, so it's odd that your offset is based on order type. The length of the substring needs to exclude the length of the location and the year as well:
SUBSTR(ORDER_NUM, LENGTH (LOCATION) + 3,
LENGTH (ORDER_NUM) - LENGTH(LOCATION) - 2 - LENGTH (SALE_TYPE))
At the moment you're getting 12 characters from your offset, when you only want six, and there are only actually eight available.
Demo with your data:
WITH CAT_ORDERS (ORDER_NUN, ORDER_DATE, ORDER_TYPE, LOCATION, SALE_TYPE) AS (
SELECT 10501702315618, TO_DATE('08/01/17', 'DD/MM/RR'), 43223, 1050, 18 FROM DUAL
UNION ALL SELECT 105017023186230, TO_DATE('21/01/17', 'DD/MM/RR'), 43221, 1050, 230 FROM DUAL
)
SELECT SUBSTR(ORDER_NUM, LENGTH (LOCATION) + 3,
LENGTH (ORDER_NUM) - LENGTH(LOCATION) - 2 - LENGTH (SALE_TYPE))
FROM CAT_ORDERS;
023156
023186
Your order number duplicates data from other columns, which doesn't seem ideal. It would be simpler to only store the 'consecutive number' part instead, and generate the full order number as a virtual column.
This should remove the sale_type characters from the end:
select
substr(substr(order_num, 0, length(order_num)-length(sale_type)), length(location) + 3)
from t;
select regexp_substr(ORDER_NUM,'^' || LOCATION || '..(.*)' || SALE_TYPE || '$',1,1,'',1)
from mytable;
Since we know now that the length of consecutive_number is fixed (namely, 6):
select regexp_substr(ORDER_NUM,'^' || LOCATION || '..(.{6})',1,1,'',1)
from mytable;
or
select regexp_substr(ORDER_NUM,'(.{6})' || SALE_TYPE || '$',1,1,'',1)
from mytable;
Since we know now that the length of consecutive_number is fixed (namely, 6):
select substr(ORDER_NUM,length(LOCATION)+3,6)
from mytable;
or
select substr(ORDER_NUM,-length(SALE_TYPE)-6,6)
from mytable;

Word Match Search with PL/SQL

what would be the best way to do a word match search in PL/SQL?
E.g. for the string "BROUGHTONS OF CHELTENHAM LIMITED"
"BROUGHTONS LIMITED" is a match
"OF LIMITED" is a match
"CHELTENHAM BROUGHTONS" is a match
"BROUG" is a non-match
Here's a rather crude approach, but should do what you are asking. As Xophmeister noted, you probably need to tokenize each string and then search the tokens (since you want to match out of order, doing a simple "like %tokenA%tokenB%tokenC%" won't work).
Also, this doesn't even touch all the issues around phonetics, soundex, etc. But again, not what you asked. This also doesn't touch performance or scaling issues, and would probably only be acceptable for a small set of data.
So, first we need a split function:
create or replace
function fn_split(i_string in varchar2, i_delimiter in varchar2 default ',', b_dedup_tokens in number default 0)
return sys.dbms_debug_vc2coll
as
l_tab sys.dbms_debug_vc2coll;
begin
select regexp_substr(i_string,'[^' || i_delimiter || ']+', 1, level)
bulk collect into l_tab
from dual
connect by regexp_substr(i_string, '[^' || i_delimiter || ']+', 1, level) is not null
order by level;
if (b_dedup_tokens > 0) then
return l_tab multiset union distinct l_tab;
end if;
return l_tab;
end;
Now we can use it to inspect strings for specific tokens. Here I'm searching for 3 tokens (John Q Public) from a sample set of data
with test_data as (
select 1 as id, 'John Q Public' as full_name from dual
union
select 2 as id, 'John John Smith' as full_name from dual
union
select 3 as id,'Sally Smith' from dual
union
select 4 as id, 'Mr John B B Q Public' from dual
union
select 5 as id, 'A Public John' from dual
)
select d.id, d.full_name, count(1) as hits
from test_data d, table(fn_split(full_name, ' ', 1))
-- should have at least 1 of these tokens
where column_value in ('John', 'Q', 'Public')
group by d.id, d.full_name
-- can also restrict results to those with at least x token hits
having count(1) >= 2
-- most hits at top of results
order by count(1) desc, id asc
Output:
"ID" "FULL_NAME" "HITS"
1 "John Q Public" 3
4 "Mr John B B Q Public" 3
5 "A Public John" 2
You can also add "upper" to make case insensitive, etc.
Use an Oracle Text index. This will allow you to issue powerful CONTAINS queries.
http://docs.oracle.com/cd/B28359_01/text.111/b28303/quicktour.htm