SELECT INTO not working as expected in procedure - oracle

Please, advice me on below procedure. below query returns null on filename_ but not sure how to get this filename_ properly.
PROCEDURE pickup( app_id IN VARCHAR2, Interval IN NUMBER, filename_ OUT VARCHAR2, status_ OUT VARCHAR2) IS
BEGIN
SELECT filename, status INTO filename_, status_
FROM (SELECT filename, status, CUSTPROFID, FILESIZE, AMP_FILE_NAME FROM INBOUND_UNCOMPLETED_PROCESS WHERE (status = 'error' or status = 'retry')
AND application_id IS NULL AND CREATEDAT < sysdate - 1/(24*60) AND (LAST_UPDATEDAT IS NULL OR LAST_UPDATEDAT < sysdate - Interval/(24*60))
order by LAST_UPDATEDAT NULLS FIRST)
WHERE ROWNUM < 2 FOR UPDATE NOWAIT;
UPDATE INBOUND SET application_id = app_id WHERE filename = filename_;
COMMIT;
EXCEPTION
WHEN NO_DATA_FOUND THEN
filename_ := NO_DATA_FOUND;
WHEN OTHERS THEN
filename_ := NULL;
END pickup;
Below query returns filename_ and status_ but I would need to add ORDER BY LAST_UPDATEDAT on the select query.
PROCEDURE pickup( app_id IN VARCHAR2, Interval IN NUMBER, filename_ OUT VARCHAR2, status_ OUT VARCHAR2) IS
BEGIN
SELECT filename, status INTO filename_, status_
FROM INBOUND WHERE (status = 'error' or status = 'retry')
AND application_id IS NULL AND CREATEDAT < sysdate - 1/(24*60) AND (LAST_UPDATEDAT IS NULL OR LAST_UPDATEDAT < sysdate - Interval/(24*60))
AND ROWNUM < 2 FOR UPDATE NOWAIT;
UPDATE INBOUND SET application_id = app_id WHERE filename = filename_;
COMMIT;
EXCEPTION
WHEN NO_DATA_FOUND THEN
filename_ := NO_DATA_FOUND;
WHEN OTHERS THEN
filename_ := NULL;
END pickup;
Thank you!

There are two errors in the procedure.
In your NO_DATA_FOUND exception handler, the line:
filename_ := NO_DATA_FOUND;
is not valid as NO_DATA_FOUND is a built-in exception and not a string. You need to change this to something like:
filename_ := 'Not found';
When you fix this then it the SELECT statement raises the exception:
ORA-02014: cannot select FOR UPDATE from view with DISTINCT, GROUP BY
This is because you can't filter by ROWNUM (or use the modern FETCH FIRST ROW ONLY syntax) alongside FOR UPDATE. This issue is addressed in this answer.
The exception is then caught by the OTHERS handler and NULL is returned.
It is bad practice to catch OTHERS. As you have found there can be many issues and catching OTHERS masks them and makes it difficult to find where the problem lies. If you need to catch exceptions then make sure you are specific about which ones need catching and then any other that gets raised is an error and should cause your code to fail and this gives you the opportunity to debug.
Similarly, using COMMIT should not be used a procedure as it prevents you grouping procedures together and issuing a ROLLBACK command to revert multiple changes at once. Instead, the block calling the procedure should COMMIT the values once all changes have been made.
The solution is to do something like this:
CREATE PACKAGE pkg_name IS
PROCEDURE pickup(
app_id IN INBOUND.APPLICATION_ID%TYPE,
Interval IN NUMBER,
filename_ OUT INBOUND.FILENAME%TYPE,
status_ OUT INBOUND.STATUS%TYPE
);
END;
/
CREATE PACKAGE BODY pkg_name IS
PROCEDURE pickup(
app_id IN INBOUND.APPLICATION_ID%TYPE,
Interval IN NUMBER,
filename_ OUT INBOUND.FILENAME%TYPE,
status_ OUT INBOUND.STATUS%TYPE
) IS
BEGIN
SELECT filename, status
INTO filename_, status_
FROM INBOUND
WHERE ROWID IN (
SELECT ROWID
FROM INBOUND
WHERE status IN ( 'error', 'retry')
AND application_id IS NULL
AND CREATEDAT < sysdate - 1/(24*60)
AND (LAST_UPDATEDAT IS NULL OR LAST_UPDATEDAT < sysdate - Interval/(24*60))
ORDER BY LAST_UPDATEDAT NULLS FIRST
FETCH FIRST ROW ONLY
)
FOR UPDATE NOWAIT;
UPDATE INBOUND SET application_id = app_id WHERE filename = filename_;
EXCEPTION
WHEN NO_DATA_FOUND THEN
filename_ := 'Not found.';
END pickup;
END;
/
Which, for the sample data:
CREATE TABLE inbound (
application_id VARCHAR2(20),
filename VARCHAR2(20),
status VARCHAR2(20),
CREATEDAT DATE,
LAST_UPDATEDAT DATE
);
INSERT INTO inbound ( filename, status, CREATEDAT, LAST_UPDATEDAT )
SELECT 'abc', 'error', SYSDATE - INTERVAL '3' HOUR, NULL FROM DUAL UNION ALL
SELECT 'def', 'retry', SYSDATE - INTERVAL '2' HOUR, SYSDATE FROM DUAL;
Then:
DECLARE
fn INBOUND.FILENAME%TYPE;
st INBOUND.STATUS%TYPE;
BEGIN
pkg_name.pickup(
app_id => 'E42',
interval => 0,
filename_ => fn,
status_ => st
);
COMMIT;
DBMS_OUTPUT.PUT_LINE( fn || ', ' || st );
END;
/
Outputs:
abc, error
and:
SELECT * FROM inbound;
outputs:
APPLICATION_ID | FILENAME | STATUS | CREATEDAT | LAST_UPDATEDAT
:------------- | :------- | :----- | :-------- | :-------------
E42 | abc | error | 21-OCT-20 | null
null | def | retry | 21-OCT-20 | 21-OCT-20
db<>fiddle here

Related

Alternative to evaluating a string using a function

I'm trying to output a list of records but some may not have a value in the subject column.
I have an altSubject column that would specify what to output instead.
simplified for example
insert all
into myTable
(id, subject, altSubject, partNumber, serialNumber, startDate, endDate)
values
(1, 'test',null,'xyz','123','1/1/2019', '1/5/2019')
into myTable
(id, subject, altSubject, partNumber, serialNumber, startDate, endDate)
values
(2, null, '''SN: '' || serialNumber','abc','789','1/1/2019', '1/5/2019')
output should look like:
subject | Part Number | Start Date | End Date
test | xyz | 1/1/2019 | 1/5/2019
SN: 789 | abc | 1/1/2019 | 1/5/2019
I've been able to do this using a case with a function below but the problem I'm having is it takes 5 minutes to run on a 40k row table.
select
...
...
case when altSubject is not null then
fAltSubject(id,altSubject)
else
subject
end subject
from
myTable
where
status = 'closed'
the function:
create or replace function fAltSubject
(pID in number
, pAltSubject in varchar2)
return varchar2
as
vNewSubject varchar2(400) := '';
begin
vSql := 'select ' ||
pAltSubject ||
' from
myTable
where
id = ' || pID;
execute immediate vSql
into
vNewSubject;
return vNewSubject;
end faltsubject;
Is there a better way to do this that doesn't take 5 minutes?
Thanks in advance.
"how to use a user defined mask in a column when the mask could be a combination of fields and text".
This is the best I can do and get good performance.
The table defines Subject, AltSubject and DisplaySubject.
A trigger sets the DisplaySubject based on the other two fields.
The trigger must reference the specific column names, so it needs to be regenerated every time columns are added. Maybe a nightly job?
create table myTable (
id number,
subject varchar2(64),
altSubject varchar2(128),
displaySubject varchar2(128),
partNumber varchar2(16),
serialNumber varchar2(16),
startDate date,
endDate date
);
create or replace procedure generate_mytable_trigger is
l_newline constant varchar2(1) := chr(10);
l_text clob := to_clob(
'create or replace trigger mytable_displaysubject
before insert or update on mytable
for each row
declare
lt_column_names sys.odcivarchar2list;
begin
if :new.subject is not null then
:new.altsubject := null;
:new.displaysubject := :new.subject;
return;
end if;
:new.displaysubject := :new.altsubject;
-- start lines to be generated');
l_end_text constant varchar2(4000) :=
'-- end lines to be generated
return;
end mytable_displaysubject;';
begin
for rec in (
select l_newline ||
':new.displaysubject := replace(:new.displaysubject, ''#'||column_name||'#'', :new.'||column_name||');'
as text
from user_tab_columns where table_name = 'MYTABLE'
and column_name not in ('SUBJECT','ALTSUBJECT','DISPLAYSUBJECT')
) loop
l_text := l_text || rec.text;
end loop;
l_text := l_text || l_newline || l_end_text;
execute immediate l_text;
end;
/
exec generate_mytable_trigger;
Now a little test:
insert into mytable(id, subject, altsubject, partnumber, serialnumber, startdate, enddate)
select 1, 'test',null,'xyz','123',sysdate, sysdate+1 from dual
union all
select 2, null,'PN: #PARTNUMBER#','abc','789',sysdate, sysdate+1 from dual
union all
select 3, null,'PN: #PARTNUMBER#, SN: #SERIALNUMBER#','qsdf','789',sysdate, sysdate+1 from dual
union all
select 3, null,'PN: #PARTNUMBER#, ??: #BADCOLUMN#','qsdf','789',sysdate, sysdate+1 from dual;
commit;
select subject, altsubject, displaysubject from mytable;
SUBJECT ALTSUBJECT DISPLAYSUBJECT
test test
PN: #PARTNUMBER# PN: abc
PN: #PARTNUMBER#, SN: #SERIALNUMBER# PN: qsdf, SN: 789
PN: #PARTNUMBER#, ??: #BADCOLUMN# PN: qsdf, ??: #BADCOLUMN#
You have a recent version of Oracle: congratulations. So use it: virtual columns!
create table myTable (
id number,
subject varchar2(32),
altSubject varchar2(64)
generated always as (case when subject is null then 'SN: '||serialnumber end),
partNumber varchar2(16),
serialNumber varchar2(16),
startDate date,
endDate date
);
insert into mytable(id, subject, partnumber, serialnumber, startdate, enddate)
select 1, 'test','xyz','123',sysdate, sysdate+1 from dual
union all
select 2, null,'abc','789',sysdate, sysdate+1 from dual;
select ID, coalesce(SUBJECT, ALTSUBJECT) subject,
PARTNUMBER, SERIALNUMBER, STARTDATE, ENDDATE
from mytable;
ID SUBJECT PARTNUMBER SERIALNUMBER STARTDATE ENDDATE
-- -------- ----------- ------------- ------------------- -------------------
1 test xyz 123 2019-12-18 13:09:10 2019-12-19 13:09:10
2 SN: 789 abc 789 2019-12-18 13:09:10 2019-12-19 13:09:10
That may very well be overkill. You could always get rid of the extra column and say:
select ID, coalesce(SUBJECT, 'SN: '||serialnumber) subject,
PARTNUMBER, SERIALNUMBER, STARTDATE, ENDDATE
from mytable;
Best regards,
Stew Ashton

Passing data when query returns value and No "EXCEPTION WHEN NO_DATA_FOUND THEN" (Oracle 11g)

I have created a procedure for updating my t_ritm table. First I have select rrcd_qnty (which is my product quantity) of a product id from t_rrcd table. Then I update the rrcd_qnty value in t_ritm table.
Here is my procedure:
procedure update_ritm_new_rate(p_oid in varchar2, p_ritm_rate in varchar2, p_euser in varchar2)
is
nrate varchar2(4);
begin
SELECT rrcd_rate into nrate
FROM (select oid, t_rrcd.rrcd_rate
from t_rrcd
where rrcd_ritm= p_oid
ORDER BY oid DESC )
WHERE rownum <= 1
ORDER BY rownum DESC ;
EXCEPTION
WHEN NO_DATA_FOUND THEN nrate := 0;
update t_ritm
set ritm_rate = nrate, euser = p_euser, edat = sysdate
where oid = p_oid;
commit;
end update_ritm_new_rate;
Some of my product id Quantity was null. so I was getting No_Data_Found error. But when and which product id has Quantity value they were successfully updating. For avoiding No_Data_Found I used EXCEPTION WHEN NO_DATA_FOUND THEN nrate := 0; which solved my no_Data_Found error. But when product id has quantity value they were not updating.
I had search lot of for this issue but not get good solution. What should be the best practice for avoiding No_Data_Found error? Could I pass my value if I don't get any No_Data_Found error?
thank in advance
That's because - if your SELECT returns something, it never reaches UPDATE as it is hidden behind the EXCEPTION handler.
Therefore, enclose it (SELECT) into its own BEGIN-END block, and put UPDATE out of it so that it is executed with whichever NRATE value is used.
PROCEDURE update_ritm_new_rate (p_oid IN VARCHAR2,
p_ritm_rate IN VARCHAR2,
p_euser IN VARCHAR2)
IS
nrate VARCHAR2 (4);
BEGIN
BEGIN --> this
SELECT rrcd_rate
INTO nrate
FROM ( SELECT oid, t_rrcd.rrcd_rate
FROM t_rrcd
WHERE rrcd_ritm = p_oid
ORDER BY oid DESC)
WHERE ROWNUM <= 1
ORDER BY ROWNUM DESC;
EXCEPTION
WHEN NO_DATA_FOUND
THEN
nrate := 0;
END; --> this
UPDATE t_ritm
SET ritm_rate = nrate, euser = p_euser, edat = SYSDATE
WHERE oid = p_oid;
COMMIT;
END update_ritm_new_rate;
I have fixed the issue by adding EXCEPTION WHEN NO_DATA_FOUND THEN nrate := 0; after the update query.
procedure update_ritm_new_rate(p_oid in varchar2, p_ritm_rate in varchar2, p_euser in varchar2)
is
nrate varchar2(4);
begin
SELECT rrcd_rate into nrate FROM (select oid, t_rrcd.rrcd_rate from t_rrcd where rrcd_ritm= p_oid ORDER BY oid DESC )
WHERE rownum <= 1 ORDER BY rownum DESC ;
update t_ritm set ritm_rate = nrate, euser = p_euser, edat = sysdate where oid = p_oid;
commit;
EXCEPTION WHEN NO_DATA_FOUND THEN nrate := 0;
end update_ritm_new_rate;

Return success/failure and error message of transaction?

I need to have an update and insert wrapped into a transaction that will be called from an external web application. I'm trying to get back a basic success/failure status when it is called along with an error message if it failed.
What I would like to do is something like the following, but it gives me the following errors:
INTO clause is expected in this SELECT statement
"SQLCODE": invalid identifier
DECLARE STATUS VARCHAR2(128);
MESSAGE VARCHAR2(128);
BEGIN
UPDATE MYTABLE
SET COL1 = 400
WHERE USERNAME = 'bigtunacan' AND pk = 12345;
INSERT INTO MYTABLE (username, col1, col2)
VALUES('bigtunacan', 400, 'foo');
SELECT 'TRUE' AS STATUS, '' AS MSG FROM MYTABLE WHERE ROWNUM = 1;
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
SELECT 'FALSE' AS STATUS, SQLCODE || SQLERRM AS MSG FROM MYTABLE WHERE ROWNUM = 1;
END;
Any Select statement inside a PL/SQL code needs an INTO clause, except the ones called from or within a cursor. In your case, You do not need to invoke any Select statement, but just assign static string values ('TRUE','FALSE') or pseudocolumns independent from SQL such as sqlcode or sqlerrm to your already defined variables.
So, consider using :
DECLARE
STATUS VARCHAR2(128) := 'TRUE';
MESSAGE VARCHAR2(128);
BEGIN
UPDATE MYTABLE
SET COL1 = 400
WHERE USERNAME = 'bigtunacan' AND pk = 12345;
INSERT INTO MYTABLE (username, col1, col2)
VALUES('bigtunacan', 400, 'foo');
-- SELECT 'TRUE' AS STATUS, '' AS MSG FROM MYTABLE WHERE ROWNUM = 1;
-- completely remove this above row, STATUS is already initialized as TRUE
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
STATUS := 'FALSE';
RAISE_APPLICATION_ERROR(-20333,'Caution : An error was encountered -
'||SQLCODE||' -ERROR- '||SQLERRM);
END;
A Commit/Rollback should not be ideally included inside a called procedure. From Tom Kyte's own words:
I wish PLSQL didn't support commit/rollback. I firmly believe transaction control MUST be done at the topmost, invoker level.
You should consider converting your anonymous block into a procedure and define the transaction control in the invoker's code.
CREATE OR REPLACE procedure yourprocedure
( p_status OUT VARCHAR2,
p_message OUT VARCHAR2
) AS
BEGIN
UPDATE mytable
SET
col1 = 400
WHERE username = 'bigtunacan' AND pk = 12345;
INSERT INTO MYTABLE (username, col1, col2)
VALUES('bigtunacan', 400, 'foo');
p_status := 'TRUE' ;
p_message := NULL;
EXCEPTION
WHEN OTHERS THEN
p_status := 'FALSE' ;
p_message := SQLERRM ;
END;
/
Invocation ( May be another block, procedure or application layer )
DECLARE
l_status VARCHAR2(20);
l_message VARCHAR2(400);
BEGIN
yourprocedure(l_status,l_message);
IF
l_status = 'TRUE'
THEN
COMMIT;
ELSE
ROLLBACK;
END IF;
END;
/
Only exception is if it's defined as an Autonomous procedure (mostly used for logging purposes) where you are supposed to commit inside the procedure.

How to return a record set from a Function in Oracle

I am trying to get the record data using function by passing values
please find the below
CREATE TABLE "TEST"
( "TEST_ID" NUMBER(9,0) NOT NULL ENABLE,
"TEST_DESC" VARCHAR2(30 BYTE),
"TEST_DATE" DATE
);
create or replace TYPE TEST_OBJ_TYPE IS OBJECT
(
TEST_ID NUMBER(9),
TEST_DESC VARCHAR(30),
dates date
);
create or replace TYPE TEST_TABTYPE AS TABLE OF TEST_OBJ_TYPE;
Using the above object and table type created the function as follows
create or replace FUNCTION GET_ROWS(dates date)RETURN TEST_TABTYPE
AS
V_Test_Tabtype Test_TabType;
table_name varchar2(30);
q1 varchar2(300);
BEGIN
table_name :='Test';
q1 := 'SELECT TEST_OBJ_TYPE(A.TEST_ID, A.TEST_DESC)FROM' || '
(SELECT TEST_ID, TEST_DESC FROM ' || table_name || ' where
TEST_DATE = '''||dates||''' ) A';
dbms_output.put_line(q1);
EXECUTE IMMEDIATE q1 BULK COLLECT INTO V_Test_TabType ;
RETURN V_Test_TabType;
EXCEPTION
WHEN OTHERS THEN
v_Test_TabType.DELETE;
RETURN v_Test_TabType;
END;
When I execute this the SQL is printing correctly but not giving the record value.
Error as follows:
select (GET_ROWS('01-08-18')) from dual
Error report -
ORA-02315: incorrect number of arguments for default constructor
ORA-06512: at "AMTEL_MIS.GET_ROWS", line 13
SELECT TEST_OBJ_TYPE(A.TEST_ID, A.TEST_DESC) FROM (SELECT TEST_ID, TEST_DESC FROM Test where TEST_DATE = '01-08-18' ) A
Please assist me further
Thanks in advance
Your type TEST_OBJ_TYPE is defined with three attributes: TEST_ID, TEST_DESC, DATES. However, your query populates the constructor with just two columns:
SELECT TEST_OBJ_TYPE(A.TEST_ID, A.TEST_DESC) FROM
You're missing a value for DATES and that's why Oracle hurls ORA-02315.
I have tried as per your suggestion but it's is giving me an error
ORA-00904: "A"."DATES": invalid identifier
Because of the convoluted way your function is written you need to include TEST_DATE (or dates) in both the subquery and the object constructor:
q1 := 'SELECT TEST_OBJ_TYPE(A.TEST_ID, A.TEST_DESC,A.TEST_DATE)FROM' || ' -- here!
(SELECT TEST_ID, TEST_DESC, TEST_DATE FROM ' -- and here!
|| table_name || ' where TEST_DATE = '''||dates||''' ) A';
If you do that your code will work. Here is a LiveSQL demo of your code with the fix. (Free Oracle login required).
As it seems likely that you will want to pass in the table name so here is a version of your code which does that:
create or replace function get_rows(dates date, p_table_name in varchar2)
return test_tabtype
as
v_test_tabtype test_tabtype;
q1 varchar2(300);
begin
q1 := 'select test_obj_type(a.test_id, a.test_desc,a.test_date) from'
|| '(select test_id, test_desc, test_date from '
|| p_table_name
|| ' where test_date = :1 ) a';
dbms_output.put_line(q1);
execute immediate q1
bulk collect into v_test_tabtype
using dates ;
return v_test_tabtype;
exception
when others then
v_test_tabtype.delete;
return v_test_tabtype;
end;
Note how much easier it is to understand the code when it is laid out with consistent use of case and regular indentation. Readability is a feature!

Oracle Stored Procedure Log File

I have a oracle procedure which selects data and insert into another table. Bellow is the code.
CREATE OR REPLACE PROCEDURE "CUSTOMER_INCREMENTAL" (
IS
BEGIN
INSERT INTO NDB_AML_CUSTOMER
(ID, TITLE,...)
SELECT ID, TITLE,...
FROM NDB_CUSTOMER_NEW
WHERE DATE_TIME > (SELECT RUN_DATE FROM CHECK_POINT WHERE TABLE_NAME = 'NDB_CUSTOMER_NEW');
UPDATE CHECK_POINT SET RUN_DATE = SYSDATE WHERE TABLE_NAME = 'NDB_CUSTOMER_NEW';
COMMIT;
END;
/
I want to know how to output the events into a table. Like Process Start Time Stamp, Process End Time Stamp & in an exception what error msg. So there will be two columns like Date and Message in the Log Table. Any suggestions?
create or replace procedure my_log (action in varchar2, message in varchar2 )
is
begin
Insert into my_log_table (ACTION, MESSAGE, EVENT_DATE)
values (action, message, sysdate);
commit;
end;
/
CREATE OR REPLACE PROCEDURE "CUSTOMER_INCREMENTAL" ()
IS
err_num NUMBER;
err_msg VARCHAR2(4000);
BEGIN
my_log ('Start','My message');
INSERT INTO NDB_AML_CUSTOMER
(ID, TITLE,...)
SELECT ID, TITLE,...
FROM NDB_CUSTOMER_NEW
WHERE DATE_TIME > (SELECT RUN_DATE FROM CHECK_POINT WHERE TABLE_NAME = 'NDB_CUSTOMER_NEW');
UPDATE CHECK_POINT SET RUN_DATE = SYSDATE WHERE TABLE_NAME = 'NDB_CUSTOMER_NEW';
COMMIT;
my_log ('End','My message');
EXCEPTION
WHEN OTHERS THEN
err_num := SQLCODE;
err_msg := SQLERRM;
my_log ('Error' , errnum ||' - ' || err_msg);
END;
/
It's much easier to log to a table, as suggested in the other answer, but rather than writing your own logging procedure, use LOGGER which is well documented and tested.
Here's an example taken from the documentation:
begin
logger.log('This is a debug message. (level = DEBUG)');
logger.log_information('This is an informational message. (level = INFORMATION)');
logger.log_warning('This is a warning message. (level = WARNING)');
logger.log_error('This is an error message (level = ERROR)');
logger.log_permanent('This is a permanent message, good for upgrades and milestones. (level = PERMANENT)');
end;
/
select id, logger_level, text
from logger_logs_5_min
order by id;
ID LOGGER_LEVEL TEXT
---- ------------ ------------------------------------------------------------------------------------------
10 16 This is a debug message. (level = DEBUG)
11 8 This is an informational message. (level = INFORMATION)
12 4 This is a warning message. (level = WARNING)
13 2 This is an error message (level = ERROR)
14 1 This is a permanent message, good for upgrades and milestones. (level = PERMANENT)

Resources