PL/SQL Procedure Syntax - sql

So i'm having a little trouble with some PL SQL statements. Essentially i'm trying to create a procedure that will check that when a new tuple is inserted, the procedure checks that there isn't another contract for the same person within the same dates ie. the dates of the new contract don't overlap the dates of the other.
Here is the code:
CREATE OR REPLACE PROCEDURE dateOrder
(name IN VARCHAR2, start IN DATE, end IN DATE)
IS
x number;
y number;
BEGIN
CREATE OR REPLACE VIEW PersonContracts AS
SELECT * FROM ContractInfo WHERE HasContract=name;
SELECT COUNT(*) INTO x FROM PersonContracts
WHERE start BETWEEN date_from AND date_to;
SELECT COUNT(*) INTO y from PersonContracts
WHERE end BETWEEN date_from AND date_to;
IF x > 0 THEN
dbms_output.put_line("overlaps.");
END IF;
IF Y > 0 THEN
dbms_output.put_line("overlaps.");
END IF;
END dateOrder;
/
BEGIN
dateOrder("John Smith", "08-oct-2014", "12-oct-2014");
END;
I have tried it with or without the view but i would prefer to keep the view if possible. I'm only new at PL!

You can't CREATE a VIEW inside a procedure using DDL (you would have to use EXECUTE IMMEDIATE to do so).
I would prefer to set the WHERE-Clause of the SELECT statement directly:
CREATE OR REPLACE PROCEDURE dateOrder (name IN VARCHAR2, start IN DATE, end IN DATE)
IS
x number;
y number;
BEGIN
SELECT COUNT(*) INTO x FROM ContractInfo WHERE HasContract=name
AND start BETWEEN date_from AND date_to;
SELECT COUNT(*) INTO y from ContractInfo WHERE HasContract=name
AND end BETWEEN date_from AND date_to;
IF x > 0 THEN
dbms_output.put_line("overlaps.");
END IF;
IF Y > 0 THEN
dbms_output.put_line("overlaps.");
END IF;
END dateOrder;
/
BEGIN
dateOrder("John Smith", "08-oct-2014", "12-oct-2014");
END;

So a few things will not work in you procedure. Take this as recommendation not as a solution:
It is not a good style to code a ddl within a procedure. And by the way to access the new view within this procedure is impossible!!
If you want to do so, put the Create View in a dynamic SQL statement like the code snippet below
All the DB Objects on which you want to access from the procedure, have to exist at compile time. So this code will never work unless you write all your Select statements also in dynamic SQL.
Don't name your parameters "start" or "end". Theese are reserved words and is therefor not allowed.
If you call the dateOrder procedure make sure that you will pass a valid date as parameters. In your example you will pass strings. Maybe this will work with your default NLS but in another environment/database it may not.
Check this out:
CREATE OR REPLACE PROCEDURE dateOrder
(name IN VARCHAR2, xstart IN DATE, xend IN DATE)
IS
x number;
y number;
BEGIN
execute immediate (
'CREATE OR REPLACE VIEW PersonContracts AS
SELECT * FROM ContractInfo ....'
);
-- that won't work, because the PersonContracts will be not there at compile time.
SELECT COUNT(*) INTO x FROM PersonContracts
WHERE start BETWEEN date_from AND date_to;
SELECT COUNT(*) INTO y from PersonContracts
WHERE end BETWEEN date_from AND date_to;
IF x > 0 THEN
dbms_output.put_line("overlaps.");
END IF;
IF Y > 0 THEN
dbms_output.put_line("overlaps.");
END IF;
END dateOrder;
BEGIN
dateOrder("John Smith", "08-oct-2014", "12-oct-2014");
END;

The view is unnecessary even if it was allowed. You want to examine only rows that has HasContract values that are equal to the name parameter. Fine, write the query just as you want it then add HasContract = name to the where clause. Don't over-think simple solutions.
Also, you can find out what you need in one query. The condition you want to catch is if there is any overlap between the interval defined by the start and stop dates and any existing start and stop dates. While we could painstakingly list out every conceivable arrangement that would lead to an overlap, let's look at the only two arrangements that don't lead to an overlap.
if the end date of one is less than or equal to the start date of the other or
if the start date of one is greater than or equal to the end data of the other.
Or, in equation form e1 <= s2 or s1 >= e2. A little Boolean magic and we can invert to e1 > s2 and s1 < e2. That gives us the simplified query:
select COUNT(*) into y
from ContractInfo
where HasContract = name
and p_end > date_from
and p_start < date_to;
If this query returns any non-zero answer, there will be an overlap somewhere. One simple query, one check afterwards. Easy.

Related

Getting unexpected values for number of days between two dates in oracle

I am writing a SQL code which fetches two dates from the database and calculates the number of days between them. Here is the code:
create table borrower(
roll_no number,
date_of_issue date,
name_of_book varchar(20),
status varchar(10)
);
insert into borrower values(1,to_date('02-JAN-2022'),'dbms','issued');
insert into borrower values(2,to_date('10-JAN-2022'),'cns','issued');
insert into borrower values(3,to_date('17-JAN-2022'),'spos','issued');
insert into borrower values(4,to_date('26-JAN-2022'),'toc','issued');
create table fine(
roll_no number,
current_date date,
amount number
);
insert into fine values(1,to_date('14-FEB-2022'),null);
insert into fine values(2,to_date('14-FEB-2022'),null);
insert into fine values(3,to_date('14-FEB-2022'),null);
insert into fine values(4,to_date('14-FEB-2022'),null);
DECLARE
roll_counter number:=1;
initial_date date;
final_date date;
date_calc number;
BEGIN
loop
select date_of_issue into initial_date from borrower where roll_no=roll_counter;
select current_date into final_date from fine where roll_no=roll_counter;
date_calc:=final_date-initial_date;
dbms_output.put_line(date_calc);
roll_counter:=roll_counter+1;
exit when roll_counter>4;
end loop;
END;
/
drop table borrower;
drop table fine;
I am not getting any error, but instead getting unexpected values for the number of days. Here is the output:
Statement processed.
246.4165625
238.4165625
231.4165625
222.4165625
I was expecting the number of days between the two dates(check the table). Can someone help me sort this out.
CURRENT_DATE is an Oracle keyword that returns the current date. Name your column something that is not an Oracle keyword.
https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CURRENT_DATE.html
As #Matthew McPeak pointed out, CURRENT_DATE is a built-in function and that function is being called rather than returning your column value.
If you want the column value then you need to prefix the column name with the table name/alias and use fine.current_date:
DECLARE
roll_counter number:=1;
initial_date date;
final_date date;
date_calc number;
BEGIN
FOR roll_counter IN 1 .. 4 LOOP
select date_of_issue
into initial_date
from borrower
where roll_no=roll_counter;
select fine.current_date
into final_date
from fine
where roll_no=roll_counter;
date_calc:=final_date-initial_date;
dbms_output.put_line(date_calc);
END LOOP;
END;
/
Which, for your sample data, outputs:
43
35
28
19
Or you can use a single query (rather than multiple queries that are called in each loop iteration):
BEGIN
FOR r IN (
SELECT f.current_date - b.date_of_issue AS diff
FROM borrower b
FULL OUTER JOIN fine f
ON (b.roll_no = f.roll_no)
WHERE COALESCE(b.roll_no, f.roll_no) BETWEEN 1 AND 4
ORDER BY COALESCE(b.roll_no, f.roll_no)
) LOOP
dbms_output.put_line(r.diff);
END LOOP;
END;
/
db<>fiddle here

Stored Procedure Functions

I have two tables
Equipment(equipmentid, equipname, equipstatus, maxrentdurationdays)EquipmentID
Equipment_Hire(equipmentID, pickupdate, dropoffdate,clientNo)
I need to create a stored function that takes clientNo as input and adds up 20$per day if the equipment_hire table's dropoffdate-pickupdate > MaxRentDurationdays
I was getting errors while doing this in oracle 11g.
CREATE OR REPLACE FUNCTION Fines
(P_ClientNo Equipment_Hire.ClientNo%TYPE)
RETURN VARCHAR2 IS
V_ClientNo INTEGER;
V_DURATIONDEFD INTEGER;
V_TOTALFINE INTEGER;
BEGIN SELECT ClientNo, MAXDURATIONDAYS
INTO V_FName, V_LName, V_TotalSalary
FROM Equipment, Equipment_hire
WHERE P_CLientNo = Equipment_Hire.ClientNo;
IF Equipment_hire.dropoffdate-equipment_hire.pickupdate >
equipment.maxdurationdays THEN
Equipment_hire.dropoffdate-equipment_hire.pickupdate*20
RETURN Equipment_hire.dropoffdate-equipment_hire.pickupdate*20; ELSE
RETURN 'Date UNSPECIFIED';
END IF; END Fines;
Data: https://www.mediafire.com/?chl6fdcl9cs817w
You can only use two tables equipment, equipment_hire.
Formatting the code helps:
CREATE OR REPLACE FUNCTION Fines(P_ClientNo Equipment_Hire.ClientNo%TYPE)
RETURN VARCHAR2 IS
V_ClientNo INTEGER;
V_DURATIONDEFD INTEGER;
V_TOTALFINE INTEGER;
BEGIN
SELECT ClientNo, MAXDURATIONDAYS
INTO V_FName, V_LName, V_TotalSalary
FROM Equipment, Equipment_hire
WHERE P_CLientNo = Equipment_Hire.ClientNo;
IF Equipment_hire.dropoffdate - equipment_hire.pickupdate > equipment.maxdurationdays THEN
Equipment_hire.dropoffdate - equipment_hire.pickupdate * 20
RETURN Equipment_hire.dropoffdate - equipment_hire.pickupdate * 20;
ELSE
RETURN 'Date UNSPECIFIED';
END IF;
END Fines;
On a quick glance:
You select two columns, but your INTO clause contains three variables.
You are cross joining the tables. Who ever told you to use this out-dated join syntax anyway?
The cross join leads to multiple result rows that you cannot read into simple variables.
What is that first line after the IF supposed to do?
You are using table names and columns outside the query.
Multiplication has precedence over subtraction (or should have). So you are taking a date, multiply it by 20 and subtract the result from another date.
You seem to think that there is just one equipment hire you are dealing with, but can't a user have several hires?

How to create sequence which start from 1 in each day

Sequence should return values 1,2,3 etc starting for 1 for every day.
current_date should used for day determination.
For example, calling today first time it shoudl return 1, in second time 2 etc.
Tomorrow, first call shoud return again 1, second call 2 etc.
Postgres 9.1 is used.
Use a table to keep the sequence:
create table daily_sequence (
day date, s integer, primary key (day, s)
);
This function will retrieve the next value:
create or replace function daily_sequence()
returns int as $$
insert into daily_sequence (day, s)
select current_date, coalesce(max(s), 0) + 1
from daily_sequence
where day = current_date
returning s
;
$$ language sql;
select daily_sequence();
Be prepared to retry in case of an improbable duplicate key value error. If previous days' sequences are not necessary delete them to keep the table and the index as light as possible:
create or replace function daily_sequence()
returns int as $$
with d as (
delete from daily_sequence
where day < current_date
)
insert into daily_sequence (day, s)
select current_date, coalesce(max(s), 0) + 1
from daily_sequence
where day = current_date
returning s
;
$$ language sql;
You just need to think of cronjob as running a shell command at a specified time or day.
Shell Command for running cron job
psql --host host.domain.com --port 32098 --db_name databaseName < my.sql
You can then just add this to your crontab (I recommend you use crontab -e to avoid breaking things)
# It will run your command at 00:00 every day
# min hour wday month mday command-to-run
0 0 * * * psql --host host.domain.com --port 32098 --db_name databaseName < my.sql
It is quite interesting task.
Lets try to use additional sequence for the date and alternative function to get next value:
-- We will use anonymous block here because it is impossible to use
-- variables and functions in DDL directly
do language plpgsql $$
begin
execute 'create sequence my_seq_day start with ' || (current_date - '1900-01-01')::varchar;
end; $$;
-- Initialize sequence
select nextval('my_seq_day');
create sequence my_seq;
create or replace function nextval_daily(in p_seq varchar) returns bigint as $$
declare
dd bigint;
lv bigint;
begin
select current_date - '1900-01-01'::date into dd;
-- Here we should to retrieve current value from sequence
-- properties instead of currval function to make it session-independent
execute 'select last_value from '||p_seq||'_day' into lv;
if dd - lv > 0 then
-- If next day has come
-- Reset main sequens
execute 'alter sequence '||p_seq||' restart';
-- And set the day sequence to the current day
execute 'alter sequence '||p_seq||'_day restart with '||dd::varchar;
execute 'select nextval('''||p_seq||'_day'')' into lv;
end if;
return nextval(p_seq);
end; $$ language plpgsql;
Then use function nextval_daily instead of nextval.
Hope it was helpful.
I have came across with almost similar requirement.
Handled the logic from query rather than modifying the sequence.
used setval() to reset the sequence to 0 if its the first entry to the table for the day.
Else nextval() of the sequence.
Below is the sample query :
SELECT
CASE WHEN NOT EXISTS (
SELECT primary_key FROM schema.table WHERE date(updated_datetime) = #{systemDate} limit 1)
THEN
setval('scheam.job_seq', 1)
ELSE
nextval('scheam.job_seq')
END
UPDATE privilege is required for the user to execute setval.
GRANT UPDATE ON ALL SEQUENCES IN SCHEMA ur_schema TO user;

Error while working with dates in pl/sql?

This is how it's supposed to work:
I have a table called Pro2 with a column called finall that is a date and another one called validade. If finall has already happened or is happening validade is set to 0.
BEGIN
FOR validade in (SELECT * FROM PRO2 WHERE TRUNC(finall)<= TRNCU(SYSDATE))
LOOP
SET validade = 0
END LOOP;
END;
I'm new in PL/SQL please help!
You have an error in your second TRUNC command.
You aren't supposed to set validade (which is a CURSOR) to any value.
Assignment operator is := and not SET in PL/SQL.
Try this:
BEGIN
FOR r_record IN (SELECT *
FROM PRO2
WHERE TRUNC(finall) <= TRUNC(SYSDATE))
LOOP
UPDATE PRO2 t1
SET t1.validade = 0
WHERE t1.id = r_record.id;
END LOOP;
END;
You shouldn't use PL/SQL for such thing!
Use SQL when it is possible!
UPDATE PRO2
SET validade = 0
WHERE TRUNC(finall) <= TRUNC(SYSDATE);
Every well-designed Oracle table has a primary key, often called id or ending with it. That is what Alex meant by t1.id. You should ask for the column(-combination) that makes the primary key for PRO2 and substitute that.
DML code like the shown UPDATE can be included in PLSQL and is really the best solution:
BEGIN
-- other things here ....
UPDATE pro2
SET validade = 0
WHERE TRUNC( finall ) <= TRUNC( SYSDATE );
-- and here ...
END;

Setting up a stored procedure in Oracle

I'm working to create a stored procedure that takes input of an id and a start and end date, then returns trips that fall within that range. I've been looking over the oracle documentation and I think I'm close, but getting a few errors yet:
CREATE or replace PROCEDURE chg_per_aircraft
(p_aircraft_id IN RCC_AIRCRAFT.aircraft_id,
p_start_date IN date,
p_end_date IN date,
p_ttl_chg_per_acft OUT INTEGER)
AS
BEGIN
SELECT RCC_AIRCRAFT.aircraft_id,
SUM(RCC_CHARTER.distance * RCC_MODEL.charge_per_mile) ttl_chg
INTO
p_aircraft_id,
p_ttl_chg_per_acft
FROM RCC_AIRCRAFT
full join RCC_CHARTER
on RCC_CHARTER.aircraft_id = RCC_AIRCRAFT.aircraft_id
left join RCC_MODEL
on RCC_MODEL.model_code = RCC_AIRCRAFT.model_code
Where RCC_CHARTER.trip_date > p_start_date and RCC_CHARTER.trip_date < p_end_date
group by RCC_AIRCRAFT.aircraft_id;
SYS.DBMS_OUTPUT.PUT_LINE(ttl_chg);
end;
Your first error is the parameter definition:
p_aircraft_id IN RCC_AIRCRAFT.aircraft_id
should be
p_aircraft_id IN RCC_AIRCRAFT.aircraft_id%TYPE
But then you're selecting INTO p_aircraft_id, which is declared as an IN parameter, so you can't set it to a new value. Is that a variable you want to pass in, or a value you want to get out? It makes more sense as something the caller supplies along with the dates, but then you'd need to use it as a filter in the select statement. If there was more than one aircraft ID - likely if it's only restricted by date - then you'd get multiple results back, which would be a too_many_rows error anyway.
Your output will only be visible to a session that is set up to handle it, so that would perhaps make more sense for the caller to do; but in any case should be:
DBMS_OUTPUT.PUT_LINE(p_ttl_chg_per_acft);
... as ttl_chg only exists as a column alias, not a PL/SQL variable.
If you are passing in the aircraft ID, you might want something like this:
CREATE or replace PROCEDURE chg_per_aircraft
(p_aircraft_id IN RCC_AIRCRAFT.aircraft_id%TYPE,
p_start_date IN date,
p_end_date IN date,
p_ttl_chg_per_acft OUT INTEGER)
AS
BEGIN
SELECT SUM(RCC_CHARTER.distance * RCC_MODEL.charge_per_mile) ttl_chg
INTO p_ttl_chg_per_acft
FROM RCC_AIRCRAFT
JOIN RCC_CHARTER
ON RCC_CHARTER.aircraft_id = RCC_AIRCRAFT.aircraft_id
JOIN RCC_MODEL
ON RCC_MODEL.model_code = RCC_AIRCRAFT.model_code
WHERE RCC_CHARTER.trip_date > p_start_date
AND RCC_CHARTER.trip_date < p_end_date
AND RCC_AIRCRAFT.aircraft_id = p_aircraft_id
GROUP BY RCC_AIRCRAFT.aircraft_id;
-- just to debug!
DBMS_OUTPUT.PUT_LINE(p_ttl_chg_per_acft);
END;
/
I've also changed to inner joins as it doesn't seem useful to make them outer joins. This would also make more sense as a function than a procedure; though wrapping a single query in a stored program may be unnecessary anyway - though this looks like an assignment.