How to dynamically use month as part of a timestamp? - sql

I'm looking to add partitions for the next X months whenever this script is run. At the moment, the script is updated manually with each run, to create partitions for the next 9 months:
ALTER TABLE AUDIT_TBL
ADD PARTITION "YEAR2020_M04" VALUES LESS THAN (TIMESTAMP' 2020-05-01 00:00:00') SEGMENT CREATION DEFERRED;
However, I'd like to get the current month, and change both the partition name and timestamp accordingly.

You can make a PL/SQL procedure with EXECUTE IMMEDIATE command to execute a string as a SQL:
CREATE OR REPLACE PROCEDURE MAKE_PARTITIONS(months_number NUMBER)
DECLARE
v_nextMonthDate DATETIME := SYSDATE;
v_year NUMBER;
v_monthCode CHAR(2);
v_limitDate DATETIME;
v_nextMonth NUMBER;
BEGIN
FOR v_nextMonth IN 0..months_number LOOP
v_year := EXTRACT(year FROM v_nextMonthDate);
v_monthCode := TO_CHAR(v_nextMonthDate, 'MM');
v_limitDate := TRUNC(ADD_MONTHS(v_nextMonthDate, 1),'mon');
EXECUTE IMMEDIATE "ALTER TABLE AUDIT_TBL ADD PARTITION 'YEAR:1_M:2' VALUES LESS THAN (TIMESTAMP ':3') SEGMENT CREATION DEFERRED"
USING v_year, v_monthCode, v_limitDate;
v_nextMonthDate := ADD_MONTHS(v_nextMonthDate, 1);
END LOOP;
END;
Note that my PL/SQL is a bit rusty, so I'm not sure if that compiles without a problem, but this is the idea.
You call this function like that:
BEGIN
MAKE_PARTITIONS(5);
END;
or like that
EXEC MAKE_PARTITIONS(5);

Related

Oracle - Trigger and Procedure compilation errors

I am trying to write a procedure to display a day of the week, but the error I get is that the "ORA-01841: The (full) year must be between -4713 and +9999 and cannot be 0".
The other problem is I made a trigger that checks if column in SCOTT.BONUS.SALARY has been updated and calculates "howmuch" - raise and returns it. It says that NEW.SAL should be declared, but how can it be declared if its a column name... ?
I think im pretty close but I am missing something, Can anyone help please? Much Appreciated.
-- trigger --
CREATE OR REPLACE TRIGGER Raise
BEFORE DELETE OR INSERT OR UPDATE ON SCOTT.BONUS
FOR EACH ROW
WHEN (NEW.SAL > 0.1*OLD.SAL)
DECLARE
howmuch number;
BEGIN
howmuch := 0.1*NEW.SAL;
dbms_output.put_line('Bonus changed to 10% - : ' || howmuch);
END;
/
-- Procedure --
CREATE OR REPLACE PROCEDURE Birth_Day(data IN varchar, Dey OUT varchar) IS
BEGIN
select to_char(date'data', 'Day') INTO Dey from dual;
END;
/
-- Starting procedure Birth_Day --
DECLARE
Dey varchar(20);
begin
Birth_Day('10/11/2020',Dey);
end;
This expression is not right:
to_char(date'data', 'Day')
The database tries to evaluate literal string 'data' as a date in ISO format ('YYYY-MM-DD'), and fails.
You need to use to_date() first to turn your variable string to a date, and then to_char():
to_char(to_date(data, 'DD/MM/YYYY'), 'Day')

delete query using execute Immediate

I want convert varchar2 into date but its occur error give me solution
DECLARE
ADATE VARCHAR2(100):='02/20/1981';
BEGIN
EXECUTE IMMEDIATE 'DELETE FROM emp WHERE HIREDATE= '||to_char(to_date(ADATE,'mm/dd/yy'),'mm/dd/yy');
DBMS_OUTPUT.PUT_LINE('DELETE');
END;
You can also use this one:
DECLARE
ADATE VARCHAR2 (100) := '02/20/1981';
BEGIN
EXECUTE IMMEDIATE
'DELETE FROM emp WHERE HIREDATE= :aDate' using TO_DATE(ADATE, 'mm/dd/yyyy');
DBMS_OUTPUT.PUT_LINE ('DELETE');
COMMIT;
END;
You can use this. Make sure to use USING clause to pass the bind variable to avoid SQL injection. Also as mentioned by #Kaushik, there is no need to do TO_CHAR.
DECLARE
ADATE VARCHAR2 (100) := '02/20/1981';
BEGIN
EXECUTE IMMEDIATE
'DELETE FROM emp WHERE HIREDATE= TO_DATE(:ADATE, ''mm/dd/yyyy'')' using adate;
DBMS_OUTPUT.PUT_LINE ('DELETE');
COMMIT;
END;
You don't need to convert it to TO_CHAR. The quotes have to be right and use proper date formats.
DECLARE
ADATE VARCHAR2(100):='02/20/1981';
BEGIN
EXECUTE IMMEDIATE 'DELETE FROM emp WHERE HIREDATE=to_date('''||ADATE||''',''mm/dd/yyyy'')';
DBMS_OUTPUT.PUT_LINE('DELETE');
END;
For your delete operation, you don't even need an EXECUTE IMMEDIATE. A block like this should be sufficient.
DECLARE
ADATE VARCHAR2(100):='02/20/1981';
BEGIN
DELETE FROM emp WHERE HIREDATE=to_date(ADATE,'mm/dd/yyyy');
DBMS_OUTPUT.PUT_LINE('DELETE');
END;

Trigger that restricts by date

Im trying to create a trigger that restricts the amount a reader can read in a given month.
CREATE OR REPLACE trigger Readings_Limit
Before update or insert on reading
for each row declare
readingcount integer;
max_read integer := 5;
Begin
Select count(*) into readingcount
from (select *
from Reading
where to_char(DateRead, 'YYYY-MM') = to_char(DateRead, 'YYYY-MM'))
where employeeid = :new.employeeid;
if :old.employeeid = :new.employeeid then
return;
else
if readingcount >= max_read then
raise_application_error (-20000, 'An Employee can only read 5 per month');
end if;
end if;
end;
This restricts the reader to 5 max in total no matter the month, i can't seem to get it to be 5 max each month. any ideas greatly appreciated!
Try to rewrite your trigger like this:
CREATE OR REPLACE trigger Readings_Limit
Before update or insert on reading
for each row
declare
readingcount integer;
max_read integer := 5;
Begin
Select count(*) into readingcount
from Reading
where DateRead between trunc(sysdate,'MM') and last_day(sysdate)
and employeeid = :new.employeeid;
if :old.employeeid = :new.employeeid then
return;
else
if readingcount >= max_read then
raise_application_error (-20000, 'An Employee can only read 5 per month');
end if;
end if;
end;
You add actual month into yout select and you avoid unnecessary date conversion.
I don't understand the condition
if :old.employeeid = :new.employeeid then
Does it mean, the trigger should not fire on updates? In this case it is better to make trigger only for insert or use clause if inserting then...
In order to properly create this validation using a trigger a procedure should be created to obtain user-specified locks so the validation can be correctly serialized in a multi-user environment.
PROCEDURE request_lock
(p_lockname IN VARCHAR2
,p_lockmode IN INTEGER DEFAULT dbms_lock.x_mode
,p_timeout IN INTEGER DEFAULT 60
,p_release_on_commit IN BOOLEAN DEFAULT TRUE
,p_expiration_secs IN INTEGER DEFAULT 600)
IS
-- dbms_lock.allocate_unique issues implicit commit, so place in its own
-- transaction so it does not affect the caller
PRAGMA AUTONOMOUS_TRANSACTION;
l_lockhandle VARCHAR2(128);
l_return NUMBER;
BEGIN
dbms_lock.allocate_unique
(lockname => p_lockname
,lockhandle => p_lockhandle
,expiration_secs => p_expiration_secs);
l_return := dbms_lock.request
(lockhandle => l_lockhandle
,lockmode => p_lockmode
,timeout => p_timeout
,release_on_commit => p_release_on_commit);
IF (l_return not in (0,4)) THEN
raise_application_error(-20001, 'dbms_lock.request Return Value ' || l_return);
END IF;
-- Must COMMIT an autonomous transaction
COMMIT;
END request_lock;
To have the least impact on scalability the serialization should be done at the finest level, which for this constraint is per employeeid and month. Types may be used in order to create variables to store this information for each row before the constraint is checked after the statement has completed. These types can be either defined in the database or (from Oracle 12c) in package specifications.
CREATE OR REPLACE TYPE reading_rec
AS OBJECT
(employeeid NUMBER(10) -- Note should match the datatype of reading.employeeid
,dateread DATE);
CREATE OR REPLACE TYPE readings_tbl
AS TABLE OF reading_rec;
The procedure and types can then be used in a compound trigger (assuming at least Oracle 11, this will need to be split into individual triggers in earlier versions)
CREATE OR REPLACE TRIGGER too_many_readings
FOR INSERT OR UPDATE ON reading
COMPOUND TRIGGER
-- Table to hold identifiers of inserted/updated readings
g_readings readings_tbl;
BEFORE STATEMENT
IS
BEGIN
-- Reset the internal readings table
g_readings := readings_tbl();
END BEFORE STATEMENT;
AFTER EACH ROW
IS
BEGIN
-- Store the inserted/updated readings
IF ( INSERTING
OR :new.employeeid <> :old.employeeid
OR :new.dateread <> :old.dateread)
THEN
g_readings.EXTEND;
g_readings(g_readings.LAST) := reading_rec(:new.employeeid, :new.dateread);
END IF;
END AFTER EACH ROW;
AFTER STATEMENT
IS
CURSOR csr_readings
IS
SELECT DISTINCT
employeeid
,trunc(dateread,'MM') monthread
FROM TABLE(g_readings)
ORDER BY employeeid
,trunc(dateread,'MM');
CURSOR csr_constraint_violations
(p_employeeid reading.employeeid%TYPE
,p_monthread reading.dateread%TYPE)
IS
SELECT count(*) readings
FROM reading rdg
WHERE rdg.employeeid = p_employeeid
AND trunc(rdg.dateread, 'MM') = p_monthread
HAVING count(*) > 5;
r_constraint_violation csr_constraint_violations%ROWTYPE;
BEGIN
-- Check if for any inserted/updated readings there exists more than
-- 5 readings for the same employee in the same month. Serialise the
-- constraint for each employeeid so concurrent transactions do not
-- affect each other
FOR r_reading IN csr_readings LOOP
request_lock('TOO_MANY_READINGS_'
|| r_reading.employeeid
|| '_' || to_char(r_reading.monthread, 'YYYYMM'));
OPEN csr_constraint_violations(r_reading.employeeid, r_reading.monthread);
FETCH csr_constraint_violations INTO r_constraint_violation;
IF csr_constraint_violations%FOUND THEN
CLOSE csr_constraint_violations;
raise_application_error(-20001, 'Employee ' || r_reading.employeeid
|| ' now has ' || r_constraint_violation.readings
|| ' in ' || to_char(r_reading.monthread, 'FMMonth YYYY'));
ELSE
CLOSE csr_constraint_violations;
END IF;
END LOOP;
END AFTER STATEMENT;
END;
You need to set the month you are looking at, so if you are considering the current month, make the inner query read like this:
( select * from Reading
where to_char(DateRead,'YYYY-MM') = to_char(DateRead,'YYYY-MM')
and to_char(sysdate,'YYYY-MM') = to_char(DateRead,'YYYY-MM'))
that way it will always compare to the current month and should move as your date moves.

Error in Oracle (PLS-00103: Encountered the symbol "="...) trying to select into a variable for use in a cursor

I'm creating a procedure. I'm selecting a max date from a table into a var, and then intending to use that var in sql for a cursor. It looks like:
l_max_update_date DATE;
--Param var
l_max_update_date := NULL;
SELECT max(pivlog.last_update_date) as maxdate into l_max_update_date
FROM piv_verification_log pivlog;
...and then...
--No data in log table? Create a max_update_date
IF l_max_update_date IS NULL
THEN
l_max_update_date := TO_TIMESTAMP('2014/SEP/1 00:00:00', 'YYYY/MON/DD HH24:MI:SS');
END IF;
Finally...
--Get affected employees (those who have certified since the max(last_update_date) listed in the log table)
CURSOR affected_employees_cursor
IS
SELECT [columns]
FROM [tables]
WHERE LAST_UPDATE_DATE > l_max_update_date;
But, whenever I compile, I get this error message:
[Error] PLS-00103 (47: 22): PLS-00103: Encountered the symbol "=" when expecting one of the following:
constant exception
table long double ref
char time timestamp
which points at this line:
l_max_update_date := NULL;
I appreciate your insight. I'm thinking it has to do with the order or location in the procedure where I'm defining the var and cursor(?).
Thank you.
You can't have executable code before the first BEGIN. It would help if you'd post all the code for your procedure, but given what can be seen above it looks to me like your procedure should be something like:
CREATE OR REPLACE PROCEDURE SOME_PROC AS
l_max_update_date DATE := NULL; -- not really needed - variables are
-- initialized to NULL if no other
-- initial value is given.
CURSOR affected_employees_cursor IS
SELECT [columns]
FROM [tables]
WHERE LAST_UPDATE_DATE > l_max_update_date;
rowAffected_employee affected_employees_cursor%ROWTYPE;
BEGIN
BEGIN
SELECT max(pivlog.last_update_date) as maxdate
into l_max_update_date
FROM piv_verification_log pivlog;
EXCEPTION
WHEN NO_DATA_FOUND THEN
l_max_update_date := NULL;
END;
--No data in log table? Create a max_update_date
IF l_max_update_date IS NULL THEN
l_max_update_date := TO_DATE('2014/SEP/1 00:00:00', 'YYYY/MON/DD HH24:MI:SS');
END IF;
OPEN affected_employees_cursor;
LOOP
FETCH affected_employees_cursor
INTO rowAffected_employee;
EXIT WHEN affected_employees_cursor%NOTFOUND;
-- do something useful with the data fetched from the cursor
END LOOP;
CLOSE affected_employees_cursor;
END SOME_PROC;
Share and enjoy.
Further to Bob's answer, the cursor will use whatever value l_max_update_date has at the point the cursor is opened, so it doesn't have to be set before the cursor is declared.
If you'd prefer that to be more obvious in your code then you could also pass the date to the cursor as a parameter:
CURSOR affected_employees_cursor (p_last_update_date DATE) IS
SELECT [columns]
FROM [tables]
WHERE LAST_UPDATE_DATE > p_max_update_date;
and then call it with:
OPEN affected_employees_cursor (l_max_update_date);
Or you could combine the lookup-up into the cursor definition, as long as you only open it once, and skip the separate look-up and check:
CREATE OR REPLACE PROCEDURE SOME_PROC AS
CURSOR affected_employees_cursor IS
SELECT [columns]
FROM [tables]
WHERE LAST_UPDATE_DATE > (
SELECT COALESCE(MAX(pivlog.last_update_date), DATE '2014-09-01')
FROM piv_verification_log pivlog
);
rowAffected_employee affected_employees_cursor%ROWTYPE;
BEGIN
OPEN affected_employees_cursor;
LOOP
FETCH affected_employees_cursor
INTO rowAffected_employee;
EXIT WHEN affected_employees_cursor%NOTFOUND;
-- do something useful with the data fetched from the cursor
END LOOP;
CLOSE affected_employees_cursor;
END SOME_PROC;
/
Or even simpler use an implicit cursor loop:
CREATE OR REPLACE PROCEDURE SOME_PROC AS
BEGIN
FOR rowAffected_employee In (
SELECT [columns]
FROM [tables]
WHERE LAST_UPDATE_DATE > (
SELECT COALESCE(MAX(pivlog.last_update_date), DATE '2014-09-01')
FROM piv_verification_log pivlog
)
)
LOOP
-- do something useful with the data fetched from the cursor
END LOOP;
END SOME_PROC;
/
Of course, depending on what you're doing with the data fetched form the cursor, this might be something that doesn't need PL/SQL at all and could be done in plain SQL.

How do I fix auto increment in oracle after a data refresh

Every time my team does a data refresh to our UAT environment we have issues with the 'auto incremented' columns in oracle They hold onto the old value and therefore cause errors when a new insert happens. The only solution I have found is to use
select test_SEQ.nextval from test_table;
Until the next sequence is bigger then the max seq number in the table. I have over 200 tables to update, is there an easier why to do this?
Thanks
Erin
One better way to do this would be to drop the sequences and create new ones with the desired START WITH value. You could generate the DDL to do this dynamically.
Check the following sqlfiddle http://sqlfiddle.com/#!4/17345/1
It doesn't completely work due to limitations in sqlfiddle, but here's the function that makes it happen:
create or replace function
reset_sequence(p_sequence_name varchar,
p_table_name varchar,
p_column_name varchar)
return integer is
v_temp integer;
v_sql varchar(2000);
begin
v_sql := 'select nvl(max('||p_column_name||'),0)+1 col_name from '||p_table_name;
execute immediate v_sql INTO v_temp;
v_sql := 'drop sequence '||p_sequence_name;
execute immediate v_sql;
v_sql := 'create sequence '||p_sequence_name||' start with '||v_temp;
execute immediate v_sql;
return v_temp;
end;
Basically you call this function and pass a schema name, table name and column name and it will set the function to the correct value. You can put a begin/exception/end block to ignore errors when dropping the sequence, in case it doesn't exist, but all that is just icing. You could also have it detect the column that is the primary key if you wanted, but no real way in Oracle to detect the sequence name.
You could also make a procedure version of this, but I tend to prefer functions for whatever reason.