PLSQL -- calling function to calculate with char(1) and number variables - sql

I'm just now teaching myself SQL and PLSQL and have haven't had too much trouble debugging the regular SQL, but I'm trying to call a PLSQL function that is throwing errors that I can't figure out from the Oracle docs and other online resources. I'm using Oracle SQL-Developer.
I've tried these but they are not helpful in this case:
How to return a record from an existing table from an Oracle PL/SQL function?
Writing a function in plsql
Oracle PLSQL function call into a second function
Can't seem to subract two numbers in a PLSQL function
I've tried writing the same function 2 different ways for practice:
Create Function Calc_Tax
(Quantity IN Number,Unit_Price IN Number,Taxable IN VarChar2)
Return Number IS amt Number;
Declare
price := Quantity * Unit_Price;
Begin
IF Taxable = 'Y' THEN
amt := price + 0.06 * price;
ELSE
amt := price;
END IF;
Return amt;
End;
-- or --
Create Or Replace Function Calc_Tax
(Quantity IN Number,Unit_Price IN Number,tax IN Sale_Item.Taxable%TYPE)
Return Number IS amt Number;
Declare
price := Quantity * Unit_Price;
Begin
IF tax = 'Y' THEN
amt := price + 0.06 * price;
ELSE
amt := price;
END IF;
Return amt;
End;
I had some trouble declaring the 'tax' parameter as a varchar2(1) so I left it just varchar2 which the docs seem to say is default for 1 space (if I read that right). Of course, I'd rather do it properly and declare the size of the varchar explicitly.
This is how I'm calling it:
DECLARE
g_last Guest.First_Name%type := 'Richard';
g_last Guest.Last_Name%type := 'Wharton';
g_id Guest.Guest_ID%type;
s_g_id Stay.Guest_ID%type;
sho_id Stay.Hotel_ID%type;
h_id Hotel.Hotel_ID%type;
h_name Hotel.Hotel_Name%type;
i_id Sale_Item.Item_ID%type;
i_name Sale_Item.Item_Name%type;
i_tax Sale_Item.Taxable%type;
cs_id Charge.Stay_ID%type;
c_date Charge.Trans_Date%type;
c_quant Charge.Quantity%type;
c_uprice Charge.Unit_Price%type;
BEGIN
SELECT H.Hotel_ID, H.Hotel_Name,C.Item_ID, Item_Name, C.Trans_Date, C.Quantity,
C.Unit_Price,Taxable INTO h_id,h_name,i_id,i_name,c_date,c_quant,c_uprice
FROM Hotel H,Charge C Where cs_id = (Select Stay_ID From Stay Where Guest_ID =
(Select Guest_ID From Guest Where First_Name='Richard' And Last_Name='Wharton'));
--WHERE id = c_id;
dbms_output.put_line
('Guest ' ||g_first || g_last || ' bought ' || i_name || ' with tax ' || Calc_Tax
(c_quant,c_uprice,i_tax));
END;
And it's throwing errors:
Error(18,1): PLS-00103: Encountered the symbol "DECLARE"
Error(43,4): PLS-00103: Encountered the symbol "end-of-file" when expecting one of the following: ( begin case declare end exception exit for goto if loop mod null pragma raise return select update while with <an identifier> <a double-quoted delimited-identifier> <a bind variable> << continue close current delete fetch lock insert open rollback savepoint set sql execute commit forall merge pipe purge
Here's the schema:
I'm just learning now, so I'm sure that any junior SQL programmers can quickly show me where I'm going wrong.

You don't use DECLARE inside a procedure or function -- use the AS keyword instead to indicate the variable declaration "block."
You also cannot put a constraint on parameters (e.g. you can have VARCHAR2 but not VARCHAR2(10) -- See Oracle docs
You also shouldn't have a semi-colon following your return statement.
Create Function Calc_Tax (Quantity IN Number, Unit_Price IN Number, Taxable IN VarChar2)
Return Number
AS
My_Variable VARCHAR2(2000);
BEGIN
-- code
END Calc_Tax;

Related

Function using dateadd to return status pl sql

I have a table named Borrowing with columns id, borrow_date and duration. I want to create a function to check the status. If someone wants to borrow the book, they give 2 parameters (v_book and v_date). v_book is the book id they want to borrow and v_date is the date they want to borrow. This function checks whether the book can be borrowed or not.
Example, user input v_book=100, and v_date='5-Jan-2020'. But in the table, the book with id 100, the borrow_date is '4-Jan-2020' and the duration is 3 days. So January 4th plus 3 days is January 7th. So that means the book cannot be borrowed by January 5th.
This is my code so far and I still got an error in the dateadd. I need to write the function using Oracle PL/SQL. Any idea? Thanks!
CREATE OR REPLACE FUNCTION check_status (v_book INT, v_date DATE) RETURN VARCHAR2;
v_duration INT;
v_borrow date;
BEGIN
SELECT duration INTO v_duration FROM Borrowing WHERE id = v_book;
SELECT borrow_date INTO v_borrow FROM Borrowing WHERE id = v_book;
SELECT DATEADD(day, v_duration, v_borrow) AS DateAdd;
IF(v_date<DateAdd) THEN
RETURN 'False';
ELSE RETURN 'True';
END IF;
END;
/
DECLARE
m_book INT:=205;
m_date DATE:='5-JAN-2020';
BEGIN
if(check_status(m_book,m_date)='True') then
dbms_output.put_line('You may borrow the book');
else then dbms_output.put_line('book not available');
END;
/
dateadd isn't an Oracle function.
If you want to add days to a date in Oracle, you simply add the number of days to the date.
E.g. 2 days from now would be sysdate + 2.
N.B. if you are assigning a date to a DATE variable, please explicitly convert strings into dates first, e.g.
m_date DATE := to_date('05/01/2020', 'dd/mm/yyyy');
By forcing a string into a DATE variable, you're forcing an implicit conversion, which uses the NLS_DATE_FORMAT parameter of your session as the format of your string, e.g. Oracle will do the following behind the scenes:
m_date DATE := to_date('5-JAN-2020', <NLS_DATE_FORMAT>);
If your NLS_DATE_FORMAT doesn't match the string you've passed in, you'll get an error. By explicitly converting, you've made your code able to run on any session, regardless of the NLS_DATE_FORMAT setting.
Apart from the many syntax errors, your function will not work as a book can be borrowed many times and you need to check all the times that it has been borrowed to make sure none of them overlap with the instant you want to start borrowing.
You want to check something like this:
CREATE OR REPLACE FUNCTION check_status (
v_book BORROWING.ID%TYPE,
v_date DATE
) RETURN VARCHAR2
IS
v_is_borrowed NUMBER(1,0);
BEGIN
SELECT COUNT(*)
INTO v_is_borrowed
FROM Borrowing
WHERE id = v_book
AND borrow_date <= v_date
AND borrow_date + duration > v_date;
RETURN CASE v_is_borrowed WHEN 0 THEN 'True' ELSE 'False' END;
END;
/
Which, for the sample data:
CREATE TABLE borrowing( id, borrow_date, duration ) AS
SELECT 205, DATE '2020-01-01', 31 FROM DUAL UNION ALL
SELECT 205, DATE '2020-02-10', 10 FROM DUAL;
Then:
BEGIN
IF check_status( 205, DATE '2020-01-05' ) = 'True' THEN
dbms_output.put_line('You may borrow the book');
ELSE
dbms_output.put_line('book not available');
END IF;
END;
/
Outputs:
book not available
db<>fiddle here
You can simplify your function code as follows:
CREATE OR REPLACE FUNCTION check_status (v_book INT, v_date DATE) RETURN VARCHAR2
IS -- this was missing in your code
v_cnt number:= 0;
BEGIN
SELECT count(1)
INTO v_cnt
FROM Borrowing
WHERE id = v_book
AND v_date between borrow_date and borrow_date + duration ;
IF(cnt > 0) THEN
RETURN 'False';
ELSE RETURN 'True';
END IF;
END;
/
Also, you can call this function using the SELECT query as follows:
Select check_status(205, date'2020-01-05')
From dual;
Please note how dates are created in oracle.
Create the database table.
create table BORROWING (ID number(3), BORROW_DATE date, DURATION number(2));
Insert a sample row.
insert into BORROWING values (100, to_date('04-01-2021','DD-MM-YYYY'), 3);
Create the function. (I changed the names and types slightly.)
create or replace function CHECK_STATUS(P_BOOK BORROWING.ID%type,
P_DATE BORROWING.BORROW_DATE%type)
return boolean
is
L_DUMMY number(1);
begin
select 1
into L_DUMMY
from BORROWING
where ID = P_BOOK
and P_DATE between BORROW_DATE and (BORROW_DATE + DURATION);
return false;
exception
when NO_DATA_FOUND then
return true;
end;
/
If the desired borrow date for the desired book falls within a period where the book is already borrowed, then the function returns false.
Test the function. (Again changed the names and types.)
declare
L_BOOK BORROWING.ID%type;
L_DATE BORROWING.BORROW_DATE%type;
begin
L_BOOK := 100;
L_DATE := to_date('05-01-2021','DD-MM-YYYY');
if CHECK_STATUS(L_BOOK, L_DATE) then
DBMS_OUTPUT.PUT_LINE('You may borrow the book.');
else
DBMS_OUTPUT.PUT_LINE('Book not available.');
end if;
end;
/
Of-course the function only checks that the book is available on the intended borrow date. The function does not check whether the book can be borrowed for the intended duration. For that, you would need to check the intended duration also.
Refer to this db<>fiddle
You can put all statements into one SELECT Statement during the creation of the function
CREATE OR REPLACE FUNCTION check_status(
i_book Borrowing.Id%type,
i_date Borrowing.Borrow_Date%type
)
RETURN VARCHAR2 IS
o_borrowed VARCHAR2(5);
BEGIN
SELECT DECODE(SIGN(COUNT(*)),0,'True','False')
INTO v_borrowed
FROM Borrowing
WHERE id = i_book
AND i_date BETWEEN borrow_date AND borrow_date + duration - 1;
RETURN o_borrowed;
END;
/
and then revoke such as
DECLARE
v_book Borrowing.Id%type := 205;
v_date Borrowing.Borrow_Date%type := date'2020-01-05';
BEGIN
IF check_status(v_book, v_date) = 'True' THEN
dbms_output.put_line('You may borrow the book');
ELSE
dbms_output.put_line('book not available');
END IF;
END;
/
where there's no predefined function called DATEADD() in Oracle database.
Alternatively, you can create a PROCEDURE as
CREATE OR REPLACE PROCEDURE check_status(
i_book Borrowing.Id%type,
i_date Borrowing.Borrow_Date%type,
o_borrowed OUT VARCHAR2
) IS
BEGIN
SELECT DECODE(SIGN(COUNT(*)),0,'You may borrow the book','book not available')
INTO o_borrowed
FROM Borrowing
WHERE id = i_book
AND i_date BETWEEN borrow_date AND borrow_date + duration - 1;
END;
/
and print the description which you want out directly as
DECLARE
v_book Borrowing.Id%type := 205;
v_date Borrowing.Borrow_Date%type := date'2020-01-05';
v_borrowed VARCHAR2(50);
BEGIN
check_status(v_book, v_date,v_borrowed);
dbms_output.put_line(v_borrowed);
END;
/

Passing Multiple Values To PL/SQL Input

I have an Oracle package which contains a function. This function has 3 inputs. I need to pass multiple values to each input. I could automate a process which runs it multiple times with each combination of variables but I would like to only make one call to the database.
Simplified Code
declare
ln_ret number;
begin
ln_ret := dbo.pkg_rpa.mis_run_script (
'%2020%',
'111','222','333','444',
'1234','2345','6192','1204'
);
dbms_output.put_line('ln_ret=' || t.t (ln_ret));
end;
CREATE OR REPLACE
package dbo.pkg_rpa IS
function mis_run_script (
p_input_dt in varchar2,
p_hospital_id in varchar2,
p_procedure_code in varchar2) RETURN number;
end PKG_RPA;
/
CREATE OR REPLACE
PACKAGE BODY dbo.pkg_rpa IS
function mis_run_claim_assessment_script (
p_input_dt in varchar2,
p_hospital_id in varchar2,
p_procedure_code in varchar2
)
Begin
for i in (select table_name from user_tables where lower(table_name) = 'temp_rpa') loop
execute immediate 'drop table temp_rpa';
end loop;
execute immediate ' create table temp_rpa as select distinct ci.claim_id, count(ci.receipt_id) as count_receipts,
sum(ci.billed_amount) as total_billed_amount, count(*) as claim_items
from claim_item ci left join claim_header ch on ch.claim_id = ci.claim_id
left join cd_hos ho on ho.hospital_id = ci.hospital_id
left join claim_type_header cl on cl.claim_id = ci.claim_id
where cl.claim_status is null and ch.deleted_flag is null
and ch.input_dt like p_input_dt
and ci.hospital_id in (p_hospital_id)
and (ci.claim_id, NVL(ci.claim_item_id,0)) in (select claim_id, NVL(claim_item_id,0) from cd_roc_claim_item
where procedure_code in (p_procedure_code))
and (ci.claim_id, NVL(ci.claim_item_id,0)) not in (select claim_id, NVL(claim_item_id,0) from cd_roc_claim_item
where procedure_code not in (p_procedure_code))
group by ci.claim_id
having sum(case when ci.service_type_id is null then 1 end) = 1)';
End;
end mis_run_script;
end PKG_RPA;
/
Pass it with quoted string (Q'<delimeter><your_actual_string><delimeter>') as follows:
begin
ln_ret := dbo.pkg_rpa.mis_run_script (
'%2020%',
Q'#'111','222','333','444'#',
Q'#'1234','2345','6192','1204'#'
);
dbms_output.put_line('ln_ret=' || t.t (ln_ret));
end;
What you could do is using an associative array as input type. Instead of varchar2, use dbms_sql.varchar2a as date type for the 2nd and 3rd arguments.
But if the arguments are related to each other, let's say
p_hospital_id '111' belongs to procedure code '1234'
p_hospital_id '222' belongs to procedure code '2345'
etc.
I think you would want to create a custom record type, create a table type of the record type and use that as an parameter.
Your arguments become p_hospital_ids in dbms_sql.varchar2a in both the package specification and the package body.
In you code, you would have to loop over it and instead of dropping the table and recreate it each time, you drop it once at the start and add data within the loop;
truncate table; --alternative drop and create
for i in 1 .. p_hospital_ids.count loop
insert into temp_rpa
select <columns>
from claim_item ci
......
and ci.hospital_id = p_hospital_ids[i]
end loop
You may want to refer to the below example which is taken from Oracle Website. Hope it helps.
CREATE OR REPLACE TYPE nt_type IS TABLE OF NUMBER;
/
CREATE OR REPLACE PROCEDURE print_nt (nt nt_type) AUTHID DEFINER IS
i NUMBER;
BEGIN
i := nt.FIRST;
IF i IS NULL THEN
DBMS_OUTPUT.PUT_LINE('nt is empty');
ELSE
WHILE i IS NOT NULL LOOP
DBMS_OUTPUT.PUT('nt.(' || i || ') = ');
DBMS_OUTPUT.PUT_LINE(NVL(TO_CHAR(nt(i)), 'NULL'));
i := nt.NEXT(i);
END LOOP;
END IF;
DBMS_OUTPUT.PUT_LINE('---');
END print_nt;
/
DECLARE
nt nt_type := nt_type(); -- nested table variable initialized to empty
BEGIN
print_nt(nt);
nt := nt_type(90, 9, 29, 58);
print_nt(nt);
END;
/
You don't need dynamic SQL at all.
CREATE OR REPLACE TYPE NUMBER_TABLE_TYPE AS TABLE OF NUMBER;
CREATE OR REPLACE TYPE VARCHAR_TABLE_TYPE AS TABLE OF VARCHAR2(1000);
function mis_run_claim_assessment_script (
p_input_dt in varchar2,
p_hospital_id in NUMBER_TABLE_TYPE, -- why on earth do you put numbers as strings?
p_procedure_code in VARCHAR_TABLE_TYPE
) RETURN ??? AS
INSERT INTO temp_rpa (...)
SELECT ...
FROM ...
WHERE ch.input_dt like p_input_dt
AND ci.hospital_id MEMBER OF p_hospital_id
AND procedure_code MEMBER OF p_procedure_code ;
RETURN ???
END;
ln_ret := mis_run_claim_assessment_script(
'%2020%',
NUMBER_TABLE_TYPE(111, 222, 333, 444),
VARCHAR_TABLE_TYPE('not','clear','in','your','question')
);

PL/SQL Error PLS-00103 - Encountered symbol xxxx when expecting yyyy

I know these errors are most often caused by typos; that's what I've been finding at least. If my problem is a typo, I cannot see it after ~30 minutes of looking, it's driving me crazy.
My question is: am I doing something fundamentally wrong, or can you see a typo?
PL/SQL:
CREATE OR REPLACE PROCEDURE Replenish_Stock(p_id VARCHAR2, n INT)
AS
no_such_id EXCEPTION;
CURSOR pc IS
SELECT Product_ID FROM Products;
BEGIN
IF p_id IN pc THEN
UPDATE Products
SET Stock_Level = Stock_Level + n
WHERE product_id = p_id;
ELSE
RAISE no_such_id;
END IF;
EXCEPTION
WHEN INVALID_NUMBER THEN
DBMS_OUTPUT.PUT_LINE('Bad integer input, ignoring procedure call.');
WHEN no_such_id THEN
DBMS_OUTPUT.PUT_LINE('Bad Product id, ignoring procedure call.');
END;
/
Error code:
Error(7,14): PLS-00103: Encountered the symbol "PC" when expecting one of the following: (
Thanks for any help.
Your usage of IN and CURSOR is not right. the below should work for you. You can just use SQL%ROWCOUNT to see if the update query impact any rows in the table.
CREATE OR REPLACE PROCEDURE Replenish_Stock(p_id VARCHAR2, n INT)
AS
no_such_id EXCEPTION;
Rows_Updated NUMBER;
BEGIN
UPDATE Products
SET Stock_Level = Stock_Level + n
WHERE product_id = p_id;
IF( SQL%ROWCOUNT = 0) THEN
RAISE no_such_id;
END IF;
EXCEPTION
WHEN INVALID_NUMBER THEN
DBMS_OUTPUT.PUT_LINE('Bad integer input, ignoring procedure call.');
WHEN no_such_id THEN
DBMS_OUTPUT.PUT_LINE('Bad Product id, ignoring procedure call.');
END;
/

PLSQL SELECT INTO FROM parameter

I created a function that should return the max id from a table(parameter)
CREATE OR REPLACE FUNCTION getmaxid
(
P_TABLE IN VARCHAR2
)
RETURN NUMBER IS
v_maxId NUMBER(38);
BEGIN
SELECT MAX(id) INTO v_maxId FROM P_TABLE;
RETURN v_maxId;
END getmaxid
However, i keep getting the error message "ORA-00942: table or view does not exist" on this line:
SELECT MAX(id) INTO v_maxId FROM P_TABLE;
Like explained earlier, you need to use dynamic SQL to perform the operation. In this case, p_table is a variable. The solution to this is to build a string that will contain the SQL and dynamically execute it one you've build the query.
The example below uses, DUAL, but the table name is arbitrary.
Here is what you're looking for, take the function outside of the block, I left it like this so that you can test it..
DECLARE
FUNCTION getmaxid (p_table IN VARCHAR2)
RETURN NUMBER
IS
v_maxid NUMBER (38);
v_select VARCHAR2 (200);
cnt SYS_REFCURSOR;
BEGIN
v_select := 'SELECT COUNT(*) FROM ' || p_table;
DBMS_OUTPUT.put_line (v_select);
EXECUTE IMMEDIATE v_select INTO v_maxid;
RETURN v_maxid;
END getmaxid;
BEGIN
DBMS_OUTPUT.put_line (getmaxid ('DUAL'));
END;

PLS-00103 Oracle stored procedure error

I am new to stored procedures.
I am trying to run stored procedure and getting these errors:
I am getting PLS-00103: Encountered the symbol "SELECT" when expecting one of the following: begin function pragma procedure...
PLS-00103: Encountered the symbol "RETURN" when expecting one of the following: * & = - + < / > at in is mod remainder not rem then...
I have tried searching for what causes these errors and for examples similar to this, but results were not sufficient. Any clues as to why these errors are happening?
here is the code:
CREATE OR REPLACE PROCEDURE LIST_ACTIONS_CHECK_ADD
(
LISTNAME IN VARCHAR2
) AS
BEGIN
DECLARE CNT NUMBER;
SELECT COUNT(LIST_NAME) INTO CNT FROM LISTS_MASTER WHERE LIST_NAME = LISTNAME;
IF (CNT > 0)
RETURN 1
ELSE
RETURN 0
END IF;
END LIST_ACTIONS_CHECK_ADD;
New Code:
CREATE OR REPLACE PROCEDURE LIST_ACTIONS_CHECK_ADD
(
P_LISTNAME IN VARCHAR2
)
AS
L_CNT NUMBER;
BEGIN
SELECT COUNT(LIST_NAME)
INTO L_CNT
FROM LISTS_MASTER
WHERE LIST_NAME = P_LISTNAME;
IF (L_CNT > 0)
RETURN 1;
ELSE
RETURN 0;
END IF;
END LIST_ACTIONS_CHECK_ADD;
The skeleton of a stored procedure declaration is
CREATE OR REPLACE PROCEDURE procedure_name( <<parameters>> )
AS
<<variable declarations>>
BEGIN
<<code>>
END procedure_name;
In the code you posted,
You put the BEGIN before the variable declarations
You have an extraneous DECLARE-- you would only use that if you are declaring a PL/SQL block that doesn't involve a CREATE.
You are missing semicolons after your RETURN statements.
A procedure cannot return a value. If you want to return either a 1 or a 0, you probably want a function, not a procedure. If you need a procedure, you can declare an OUT parameter.
You are missing the THEN after the IF
It sounds like you want something like
CREATE OR REPLACE FUNCTION LIST_ACTIONS_CHECK_ADD
(
LISTNAME IN VARCHAR2
)
RETURN NUMBER
AS
CNT NUMBER;
BEGIN
SELECT COUNT(LIST_NAME)
INTO CNT
FROM LISTS_MASTER
WHERE LIST_NAME = LISTNAME;
IF (CNT > 0)
THEN
RETURN 1;
ELSE
RETURN 0;
END IF;
END LIST_ACTIONS_CHECK_ADD;
Note that as a general matter, you are generally better off using some sort of naming convention to ensure that parameters and local variables do not share the name of a column. Trying to figure out whether LISTNAME is a parameter or a column name and what the difference between LIST_NAME and LISTNAME is will generally confuse future programmers. Personally, I use a p_ prefix for parameters and a l_ prefix for local variables. I would also suggested using anchored types-- lists_master.list_name%type if that is what is being passed in
CREATE OR REPLACE FUNCTION LIST_ACTIONS_CHECK_ADD
(
P_LIST_NAME IN lists_master.list_name%type
)
RETURN NUMBER
AS
L_CNT NUMBER;
BEGIN
SELECT COUNT(LIST_NAME)
INTO L_CNT
FROM LISTS_MASTER
WHERE LIST_NAME = P_LIST_NAME;
IF (L_CNT > 0)
THEN
RETURN 1;
ELSE
RETURN 0;
END IF;
END LIST_ACTIONS_CHECK_ADD;
(Correction #1) You cannot return a value in a procedure; LIST_ACTIONS_CHECK_ADD should be dropped and declared as a function in order to return a NUMBER
(Correction #2) You need to move the declaration of CNT as follows (see below)
(Correction #3) You need semicolons on the return statements:
(Correction #4) You need a THEN after IF (CNT > 0) (see below):
DROP PROCEDURE LIST_ACTIONS_CHECK_ADD;
CREATE OR REPLACE FUNCTION LIST_ACTIONS_CHECK_ADD
(
LISTNAME IN VARCHAR2
)
RETURN NUMBER AS
CNT NUMBER;
BEGIN
SELECT COUNT(LIST_NAME) INTO CNT FROM LISTS_MASTER WHERE LIST_NAME = LISTNAME;
IF (CNT > 0) THEN
RETURN 1;
ELSE
RETURN 0;
END IF;
END LIST_ACTIONS_CHECK_ADD;
This Can be executed from SQLPLUS as:
SET SERVEROUTPUT ON SIZE 100000;
DECLARE
V_RESULT NUMBER;
BEGIN
V_RESULT := LIST_ACTIONS_CHECK_ADD('X');
DBMS_OUTPUT.PUT_LINE('RESULT: ' || V_RESULT);
END;