Is is possible to start and Oracle SQL Procedure with a CTE? - oracle

I have a CTE that I know works, but I want to use it as a procedure so I can parameterize some of the queries within it. Here is the code I tried to run to create the procedure
CREATE OR REPLACE PROCEDURE VATIS_OWNER.getEnvVariables(PF IN VARCHAR, SN IN VARCHAR) AS
BEGIN
--Joins environemt variables from TESTCASES and ENVIRONMENTS tables and transposes result into columns 'ColumnName' and 'ColumnValue' and the ProfileId
--Is used by getAllDynamicData and unioned with results from sp_getProfileDetails
with
cte as (
SELECT
T.TestcaseID,
T.ProfileID,
T.TrustID,
T.DNIS,
T.TESTID,
T.ACDID,
--Must be 8 digits long, so leading 0's are added
SUBSTR(CONCAT('00000000',T.testcaseid),-8, 8) as TestCaseNo,
T.HostId,
E.TFN
FROM TESTCASES T
FULL JOIN ENVIRONMENTS E ON T.ENV_NAME = E.ENV_NAME
--Only returns env vars associated with testcase #SN
where T.TestcaseID = SN
),
cte2 as (
Select A.ProfileID
,B.*
From cte A
--Transpose happens here
Cross Apply ( SELECT 'TestcaseID' AS ColumnName,A.TestcaseID AS ColumnValue FROM DUAL UNION ALL
SELECT 'ProfileID' AS ColumnName,A.ProfileID AS ColumnValue FROM DUAL UNION ALL
SELECT 'TrustID' AS ColumnName,A.TrustID AS ColumnValue FROM DUAL UNION ALL
SELECT 'DNIS' AS ColumnName,SUBSTR(A.DNIS,-7,7) AS ColumnValue FROM DUAL UNION ALL
SELECT 'TESTID' AS ColumnName,A.TESTID AS ColumnValue FROM DUAL UNION ALL
SELECT 'ACDID' AS ColumnName,A.ACDID AS ColumnValue FROM DUAL UNION ALL
SELECT 'TestCaseNo' AS ColumnName,A.TestCaseNo AS ColumnValue FROM DUAL UNION ALL
SELECT 'HostId' AS ColumnName,A.HostId AS ColumnValue FROM DUAL UNION ALL
SELECT 'TFN' AS ColumnName,A.TFN AS ColumnValue FROM DUAL UNION ALL
SELECT 'INVALIDANI' AS ColumnName,SUBSTR(CONCAT(A.TestcaseID ,A.TESTID),-10,10) AS ColumnValue FROM DUAL
) B
)
select distinct * from cte2 where profileID = PF;
END getEnvVariables;
If I replace SN and PF with string values and run just the CTE and query, it works. And an equivalent version works as a Stored Procedure in SQL Server, but when I try to create this procedure in Oracle, I get this compile error:
PLS-00428: an INTO clause is expected in this SELECT statement
Any idea why I can't use this in a procedure? I am more familiar with SQL Server than Oracle, so if I've forgotten something please let me know. Thanks in advance.

If you want to return the select result from this procedure, You have to use a SYS_REFCURSOR as below -
CREATE OR replace PROCEDURE vatis_owner.Getenvvariables(pf IN VARCHAR,
sn IN VARCHAR,
res OUT SYS_REFCURSOR) AS
BEGIN
--Joins environemt variables from TESTCASES and ENVIRONMENTS tables and transposes result into columns 'ColumnName' and 'ColumnValue' and the ProfileId
--Is used by getAllDynamicData and unioned with results from sp_getProfileDetails
OPEN res FOR
WITH cte AS
(
SELECT t.testcaseid,
t.profileid,
t.trustid,
t.dnis,
t.testid,
t.acdid,
--Must be 8 digits long, so leading 0's are added
Substr(Concat('00000000',t.testcaseid),-8, 8) AS testcaseno,
t.hostid,
e.tfn
FROM testcases t
full join environments e
ON t.env_name = e.env_name
--Only returns env vars associated with testcase #SN
WHERE t.testcaseid = sn ), cte2 AS
(
SELECT a.profileid ,
b.*
FROM cte a
--Transpose happens here
cross apply
(
SELECT 'TestcaseID' AS columnname,
a.testcaseid AS columnvalue
FROM dual
UNION ALL
SELECT 'ProfileID' AS columnname,
a.profileid AS columnvalue
FROM dual
UNION ALL
SELECT 'TrustID' AS columnname,
a.trustid AS columnvalue
FROM dual
UNION ALL
SELECT 'DNIS' AS columnname,
substr(a.dnis,-7,7) AS columnvalue
FROM dual
UNION ALL
SELECT 'TESTID' AS columnname,
a.testid AS columnvalue
FROM dual
UNION ALL
SELECT 'ACDID' AS columnname,
a.acdid AS columnvalue
FROM dual
UNION ALL
SELECT 'TestCaseNo' AS columnname,
a.testcaseno AS columnvalue
FROM dual
UNION ALL
SELECT 'HostId' AS columnname,
a.hostid AS columnvalue
FROM dual
UNION ALL
SELECT 'TFN' AS columnname,
a.tfn AS columnvalue
FROM dual
UNION ALL
SELECT 'INVALIDANI' AS columnname,
substr(concat(a.testcaseid ,a.testid),-10,10) AS columnvalue
FROM dual ) b )
SELECT DISTINCT *
FROM cte2
WHERE profileid = pf;
END getenvvariables;
Then later on you can use this proc to return result in a ref_cursor variable.
DECALRE
RES SYS_REFCURSOR;
BEGIN
vatis_owner.Getenvvariables(pf,
sn,
RES);
FOR I IN 1..RES.COUNT LOOP
DBMS_OUTPUT.PUT_LINE(I.YOUR_DESIRED_COLUMNS);
END LOOP;
END;
I can see that you are using CROSS APPLY in your query that means you must be using version 12C or higher. So you can use DBMS_SQL.RETURN_RESULT function -
CREATE OR replace PROCEDURE vatis_owner.Getenvvariables(pf IN VARCHAR,
sn IN VARCHAR) AS
RES SYS_REFCURSOR;
BEGIN
--Joins environemt variables from TESTCASES and ENVIRONMENTS tables and transposes result into columns 'ColumnName' and 'ColumnValue' and the ProfileId
--Is used by getAllDynamicData and unioned with results from sp_getProfileDetails
OPEN RES FOR
WITH cte AS
(
SELECT t.testcaseid,
t.profileid,
t.trustid,
t.dnis,
t.testid,
t.acdid,
--Must be 8 digits long, so leading 0's are added
Substr(Concat('00000000',t.testcaseid),-8, 8) AS testcaseno,
t.hostid,
e.tfn
FROM testcases t
full join environments e
ON t.env_name = e.env_name
--Only returns env vars associated with testcase #SN
WHERE t.testcaseid = sn ), cte2 AS
(
SELECT a.profileid ,
b.*
FROM cte a
--Transpose happens here
cross apply
(
SELECT 'TestcaseID' AS columnname,
a.testcaseid AS columnvalue
FROM dual
UNION ALL
SELECT 'ProfileID' AS columnname,
a.profileid AS columnvalue
FROM dual
UNION ALL
SELECT 'TrustID' AS columnname,
a.trustid AS columnvalue
FROM dual
UNION ALL
SELECT 'DNIS' AS columnname,
substr(a.dnis,-7,7) AS columnvalue
FROM dual
UNION ALL
SELECT 'TESTID' AS columnname,
a.testid AS columnvalue
FROM dual
UNION ALL
SELECT 'ACDID' AS columnname,
a.acdid AS columnvalue
FROM dual
UNION ALL
SELECT 'TestCaseNo' AS columnname,
a.testcaseno AS columnvalue
FROM dual
UNION ALL
SELECT 'HostId' AS columnname,
a.hostid AS columnvalue
FROM dual
UNION ALL
SELECT 'TFN' AS columnname,
a.tfn AS columnvalue
FROM dual
UNION ALL
SELECT 'INVALIDANI' AS columnname,
substr(concat(a.testcaseid ,a.testid),-10,10) AS columnvalue
FROM dual ) b )
SELECT DISTINCT *
FROM cte2
WHERE profileid = pf;
DBMS_SQL.RETURN_RESULT(RES);
END getenvvariables;
When you will run this proc, You will see the result on console. For further information, Please read.

Related

Group By inside Rtrim(Xmlagg (Xmlelement (e,element || ',')).extract ( '//text()' ).GetClobVal(), ',')

I need to group values ​​inside a query using (or not) the command Rtrim(Xmlagg (Xmlelement (e,column || ',')).extract ( '//text()' ).GetClobVal(), ','), but I can't find any literature where explain a way to group the data inside this command. The code is very simple, as you can see below:
SELECT ID,
Rtrim(Xmlagg (Xmlelement (and, CONTRACTS || ',')).extract ( '//text()' ).GetClobVal(), ',') AS CONTRACTS
FROM TABLE_A
GROUP BY ID
The result in CONTRACTS is always repeated when the ID is found, thats ok, it´s working!
ID
CONTRACTS
876
1,1,1,2,3,3
But what I really need is this return:
ID
CONTRACTS
876
1,2,3
It´s not necessary to use the command Rtrim(Xmlagg (Xmlelement (e,column || ',')).extract ( '//text()' ).GetClobVal(), ','), instead, I just use to concatenate element with comma "," in the same column.
If anyone can help me, I would be very grateful!
If your values will fit into a VARCHAR2 data type (rather than a CLOB) then you can use a nested sub-query to get the DISTINCT values for each ID:
SELECT ID,
LISTAGG(contracts, ',') WITHIN GROUP (ORDER BY contracts) AS CONTRACTS
FROM ( SELECT DISTINCT id, contracts FROM TABLE_A)
GROUP BY ID
Or, from Oracle 19c, it is built-in to LISTAGG:
SELECT ID,
LISTAGG(DISTINCT contracts, ',') WITHIN GROUP (ORDER BY contracts) AS CONTRACTS
FROM TABLE_A
GROUP BY ID
If you want a CLOB then you can use the same technique as the first query:
SELECT ID,
Rtrim(
Xmlagg(
Xmlelement(name, CONTRACTS || ',')
ORDER BY contracts
).extract ( '//text()' ).GetClobVal(),
','
) AS CONTRACTS
FROM (SELECT DISTINCT id, contracts FROM TABLE_A)
GROUP BY ID
Which, for the sample data:
CREATE TABLE table_a (id, contracts) AS
SELECT 876, 1 FROM DUAL UNION ALL
SELECT 876, 1 FROM DUAL UNION ALL
SELECT 876, 1 FROM DUAL UNION ALL
SELECT 876, 2 FROM DUAL UNION ALL
SELECT 876, 2 FROM DUAL UNION ALL
SELECT 876, 3 FROM DUAL UNION ALL
SELECT 876, 3 FROM DUAL UNION ALL
SELECT 876, 3 FROM DUAL;
All output:
ID
CONTRACTS
876
1,2,3
db<>fiddle here
It's much easier to do all those operation in XML functions: DBFiddle
SELECT--+ NO_XML_QUERY_REWRITE
id,
xmlquery(
'string-join(distinct-values($R/R/X/text()),",")'
passing
Xmlelement(
R,
Xmlagg(
Xmlelement (X, CONTRACTS)
order by CONTRACTS
)) as R
RETURNING CONTENT
) AS CONTRACTS
FROM TABLE_A
GROUP BY ID;
Full example with test data:
with table_a (id, contracts) AS (
SELECT 876, 1 FROM DUAL UNION ALL
SELECT 876, 1 FROM DUAL UNION ALL
SELECT 876, 1 FROM DUAL UNION ALL
SELECT 876, 2 FROM DUAL UNION ALL
SELECT 876, 2 FROM DUAL UNION ALL
SELECT 876, 3 FROM DUAL UNION ALL
SELECT 876, 3 FROM DUAL UNION ALL
SELECT 876, 3 FROM DUAL
)
SELECT--+ NO_XML_QUERY_REWRITE
id,
xmlquery(
'string-join(distinct-values($R/R/X/text()),",")'
passing
Xmlelement(
R,
Xmlagg(
Xmlelement (X, CONTRACTS)
order by CONTRACTS
)) as R
RETURNING CONTENT
) AS CONTRACTS
FROM TABLE_A
GROUP BY ID;

REGEXP_SUBSTR not able to process only current row

(SELECT LISTAGG(EVENT_DESC, ',') WITHIN GROUP (ORDER BY EVENT_DESC) FROM EVENT_REF WHERE EVENT_ID IN
( SELECT REGEXP_SUBSTR(AFTER_VALUE,'[^,]+', 1, level) FROM DUAL
CONNECT BY REGEXP_SUBSTR(AFTER_VALUE, '[^,]+', 1, level) IS NOT NULL
)
)
A table from which I am fetching AFTER_VALUE has values of integer which is comma seperated like
AFTER_VALUE data
Expected output
1
Event1
1,2
Event1,Event2
1,12,2,5
Event1,Event12,Event2,Event5
15,13
Event15,Event13
these are Ids in EVENT_REF table which have some description. I am trying to basically present
ex. 1,2 as Event1, Event2 and send back from query. There are multiple events so using REPLACE would be very tedious.
When using above query I'm getting error as “ORA-01722: invalid number” whenever there is more than one value in AFTER_VALUE column Ex. if there exists only one id , then the query works but for values like 1,2 or 1,13 etc it throws invalid number error.
PS: The event names are not Event1,Event2 etc , I have just put for reference.
You don't even need regular expressions for this assignment. Standard string function replace() can do the same thing, and faster. You only need an extra 'Event' at the beginning of the string, since that one doesn't "replace" anything.
Like this: (note that you don't need the with clause; I included it only for quick testing)
with
event_ref (after_value) as (
select '1' from dual union all
select '1,2' from dual union all
select '1,12,2,5' from dual union all
select '15,13' from dual
)
select after_value,
'Event' || replace(after_value, ',', ',Event') as desired_output
from event_ref
;
AFTER_VALUE DESIRED_OUTPUT
----------- -----------------------------
1 Event1
1,2 Event1,Event2
1,12,2,5 Event1,Event12,Event2,Event5
15,13 Event15,Event13
Ah,ok, looks, like you have other characters in your comma-separated list, so you can use this query:
with EVENT_REF(EVENT_ID,EVENT_DESC) as (
select 1, 'Desc 1' from dual union all
select 2, 'Desc 2' from dual union all
select 3, 'Desc 3' from dual union all
select 4, 'Desc 4' from dual union all
select 5, 'Desc 5' from dual union all
select 12, 'Desc12' from dual union all
select 13, 'Desc13' from dual union all
select 15, 'Desc15' from dual
)
select
(SELECT LISTAGG(EVENT_DESC, ',')
WITHIN GROUP (ORDER BY EVENT_DESC)
FROM EVENT_REF
WHERE EVENT_ID IN
( SELECT to_number(REGEXP_SUBSTR(AFTER_VALUE,'\d+', 1, level))
FROM DUAL
CONNECT BY level<=REGEXP_COUNT(AFTER_VALUE, '\d+')
)
)
from (
select '1' AFTER_VALUE from dual union all
select '1,2' AFTER_VALUE from dual union all
select '1,12,2,5' AFTER_VALUE from dual union all
select '15,13' AFTER_VALUE from dual
);
PS. And do not forget that to_number has 'default on conversion error' now: https://docs.oracle.com/en/database/oracle/oracle-database/12.2/sqlrf/TO_NUMBER.html
There is no need to split and concatenate substrings, just use regexp_replace:
with EVENT_REF (AFTER_VALUE) as (
select '1' from dual union all
select '1,2' from dual union all
select '1,12,2,5' from dual union all
select '15,13' from dual
)
select regexp_replace(AFTER_VALUE,'(\d+)','Event\1') from EVENT_REF;
REGEXP_REPLACE(AFTER_VALUE,'(\D+)','EVENT\1')
-----------------------------------------------
Event1
Event1,Event2
Event1,Event12,Event2,Event5
Event15,Event13

sql placeholder rows

I have an apex item P_USERS which can have a value higher than the amount of rows returning from the query below.
I have a classic report which has the following query:
select
first_name,
last_name
from accounts
where account_role = 'Author'
order by account_nr;
I want placeholder rows to be added to the query (first_name = null, last_name = null etc.), if the total rows from the query is lesser than the value in the apex_item P_USERS.
Any tips on how to achieve this? Maybe with a LEFT join?
If you have more result than the minima you defined, you must add the rest with union.
Here is what you could try to adapt to your case:
SELECT i,c FROM (
select rownum i, c from (
select 'a' c from dual union all select 'b' from dual union all select 'd' from dual union all select 'be' from dual
)), (Select Rownum r From dual Connect By Rownum <= 3)
where (i(+)= r)
union select i,c from (select rownum i, c from (
select 'a' c from dual union all select 'b' from dual union all select 'd' from dual union all select 'be' from dual
)) where i>3
You may try to use a LEFT JOIN.
First, create a list of number until the limit you want like suggested here:
-- let's say you want 300 records
Select Rownum r From dual Connect By Rownum <= 300
Then you can use this to left join and have empty records:
SELECT C, R FROM
( select rownum i, c from (select 'a' c from dual union all select 'b' from dual) )
, ( Select Rownum r From dual Connect By Rownum <= 300)
where i(+)= r order by r
The above gives you an ordered list starting with 'a', 'b', then null until the end.
So you could adapt it to your case so:
SELECT F,L FROM
( select rownum i, f, l from (
select first_name f, last_name l
from accounts where account_role = 'Author'
order by account_nr) )
, ( Select Rownum r From dual Connect By Rownum <= 300)
where i(+)= r

Passing a delimited string in the NOT IN clause

The below SQL conceptually replicates the problem I'm trying to solve. Despite passing a NOT IN clause all three records are returned.
SELECT * FROM (
SELECT 'JACK' AS VALUE FROM DUAL
UNION
SELECT 'JOHN' AS VALUE FROM DUAL
UNION
SELECT 'BOB' AS VALUE FROM DUAL
) WHERE VALUE NOT IN (SELECT 'BOB,JOHN' FROM DUAL);
I have a table that holds a delimited string that I want to use as the criteria to exclude records from the dataset. However, the problem I have is that the returned string is not broken down into its delimited items. I want the above to translate to:
SELECT * FROM (
SELECT 'JACK' AS VALUE FROM DUAL
UNION
SELECT 'JOHN' AS VALUE FROM DUAL
UNION
SELECT 'BOB' AS VALUE FROM DUAL
) WHERE VALUE NOT IN ('BOB','JOHN');
You can use regexp_substr for that problem:
SELECT * FROM (
SELECT 'JACK' AS VALUE FROM DUAL
UNION
SELECT 'JOHN' AS VALUE FROM DUAL
UNION
SELECT 'BOB' AS VALUE FROM DUAL
)
WHERE VALUE NOT IN (SELECT regexp_substr('BOB,JOHN','[^,]+', 1, LEVEL) FROM dual CONNECT BY regexp_substr('BOB,JOHN', '[^,]+', 1, LEVEL) IS NOT NULL)
'BOB,JOHN' is not a list of two string values it is one string value that just happens to contain a comma in the string and:
'JACK' = 'BOB,JOHN'
'JOHN' = 'BOB,JOHN'
'BOB' = 'BOB,JOHN'
Are all false so your query will return all rows as matched by the NOT IN filter.
You can surround your values and list with the delimiter characters and test whether the value is not a sub-string of the list like this:
SELECT *
FROM (
SELECT 'JACK' AS VALUE FROM DUAL UNION ALL
SELECT 'JOHN' AS VALUE FROM DUAL UNION ALL
SELECT 'BOB' AS VALUE FROM DUAL
)
WHERE INSTR( ',' || 'BOB,JOHN' || ',', ',' || value || ',' ) = 0
Or you can use a user-defined collection:
CREATE OR REPLACE TYPE stringlist IS TABLE OF VARCHAR2(20);
Then use the MEMBER OF operator to test whether a value is a member of the collection:
SELECT *
FROM (
SELECT 'JACK' AS VALUE FROM DUAL UNION ALL
SELECT 'JOHN' AS VALUE FROM DUAL UNION ALL
SELECT 'BOB' AS VALUE FROM DUAL
)
WHERE VALUE NOT MEMBER OF StringList( 'BOB', 'JOHN' );

Oracle - Select all values from in clause even if there is no match

I have a dynamic data set such as 'AAA','TTT','CCC','FFF'
I need to match this data against a column C in a table T
e.g. I have in the table T for Column C, 'AAA','BBB','DDD','FFF'
I need to return something like (show null if the value doesn't exist in Column)
'AAA'
'TTT' NULL
'CCC' NULL
'FFF'
I don't want to drop the set into table as my data changes frequently and need to query quickly.
Any ideas greatly appreciated.
Is this what you're after ??
with w_data as (
select 'AAA' c from dual union all
select 'TTT' c from dual union all
select 'CCC' c from dual union all
select 'FFF' c from dual
),
w_table_t as (
select 'AAA' c from dual union all
select 'BBB' c from dual union all
select 'DDD' c from dual union all
select 'FFF' c from dual
)
select d.c,
NVL2(t.c, '', 'NULL' )
from w_data d
LEFT OUTER JOIN
w_table_t t
ON t.c = d.c
/
results:
C NVL2
--- ----
AAA
FFF
TTT NULL
CCC NULL
Try this (EDITED) :
with data_set as (
select 'AAA' col from dual union
select 'TTT' col from dual union
select 'CCC' col from dual union
select 'FFF' col from dual
)
select case when d.col in (select column_C from table_T) then d.col
else d.col||' Null' end as col_name
from data_set d
/

Resources