PL/SQL trigger giving error? - database

Got this question for an exam but getting error from trigger. Please help.
Question
customer(cust_id,cust_name,address)
rent_info(cust_id,date_out,date_due_in,date_returned,fine)
rented_video(cust_id,no_of_videos)
Issuedate should be current date and due date should be 7 days after issue date. When video is returned by the customer the rent_info table should contain cust_id, date_out, date_due_in. A trigger is used to insert data into rent_info table. The following validation must be done before inserting data.
A customer is not allowed to take more than 3 videos in the same date. When a video is returned the returm date is updated. And fine is calculated if any.
Fine is calculated by
For the first three days of delay Rs
10 per day
For the follwoing three days of
delay Rs 20 per day
After the six dates charge fine of
Rs 30 per day
Write a procedure/trigger for doing the updation operation.
I solved it like this.
Made three table:
create table customer(
cust_id number(4),
cust_name varchar2(8),
address varchar2(8)
);
create table rent_info(
cust_id number(4),
date_out date,
date_due_in date,
date_returned date,
fine number(10)
);
create table rented_video(
cust_id number(4),
no_vid number(4)
);
Procedure for taking book
create or replace procedure take_proc(c_id in int,d_out in date) is
val number(3) :=0;
begin
insert into rent_info values(c_id,d_out,d_out+7,NULL,0);
update rented_video set no_vid=no_vid+1 where cust_id=c_id;
--val := select count(date_out) from rent_info where (date_out='12-jan-2010');
--dbms_output.put_line('Values is '||val);
end;
/
Procedure for returning book
create or replace procedure return_proc(c_id in int,d_ret in date) is
val number(3) :=0;
begin
update rented_video set no_vid=no_vid-1 where cust_id=c_id;
update rent_info set date_returned=d_ret where cust_id=c_id;
--insert into rent_info values(c_id,d_out,d_out+7,NULL,0);
update rented_video set no_vid=no_vid-1 where cust_id=c_id;
--val := select count(date_out) from rent_info where (date_out='12-jan-2010');
--dbms_output.put_line('Values is '||val);
end;
/
Trigger for updating Fine when book is returned
create or replace trigger ret_trig
before update on rent_info
for each row
declare
tfine number(7) := 0;
temp number(7) := 0;
rdate date;
dudate date;
cid number(4);
begin
--select date_returned into rdate from rent_info;
--select date_due_in into dudate from rent_info;
--select cust_id into cid from rent_info;
--if (rdate- dudate) <=3 then
--temp := rdate- dudate;
--tfine := tfine+ temp * 10;
--end if;
if (:new.date_returned-:old.date_due_in ) <=3 then
temp := :new.date_returned-:old.date_due_in;
tfine := tfine+ temp * 10;
dbms_output.put_line('Fine Values is '|| tfine);
elsif (:new.date_returned-:old.date_due_in ) <=6 then
temp := :new.date_returned-:old.date_due_in;
tfine := tfine+ 3 * 10;
tfine := tfine+ 20*(temp-3);
dbms_output.put_line('Fine Values is '|| tfine);
else
temp := :new.date_returned-:old.date_due_in;
tfine := tfine+ 3 * 10;
tfine := tfine+ 3 * 20;
tfine := tfine+ 30*(temp-6);
dbms_output.put_line('Fine Values is '|| tfine);
end if;
--update rent_info set fine=fine+tfine where cust_id=:old.cust_id;
end;
/
I could calculate the fine correctly but couldnt update it to the rent_info table ( Last line of the trigger which does the updation is commented).
I Think I have made some logical mistake in trigger creation. Please tell my how to solve the problem correctly.
Sample values to insert
insert into customer values(1,'john','abc h');
insert into customer values(2,'joseph','cde h');
insert into rented_video values(1,0);
insert into rented_video values(2,0);
exec take_proc(1,'12-jan-2010');
exec take_proc(2,'13-jan-2010');
exec return_proc(1,'16-jan-2010');
exec return_proc(2,'29-jan-2010');

Inside a row-level trigger, you change the value of a column by simply doing an assignment to :NEW - you don't issue an UPDATE statement. e.g.:
:NEW.fine := :OLD.fine + tfine;

Why are you using a trigger? You are building a very clear API by developing these procedures, but then you shove code into a trigger. Why not incorporate this logic into return_proc to make things more obvious?

Related

Insert or Update in an DBLINK as a Loop

I have a Row, which i wannt to Insert (or Update if the ID is already used) from another Schema with an DBLINK. This Table has many FKs, so i need to Update or Insert(if doesnt exist) around 20 Tables.
I tried it by writting all needed Rows from the different Tables from the DBLINK into Variables and then checked if the Targeted Tables had this rows already. Im sure that there is a much easier way to do it. A loop where the tables and their rowtypes are saved in an Collection with execute immdediate for example, but i couldnt find anything.
This example is like 20 times in my Code and im sure that there is an easier way to do it then writting this over and over.
PROCEDURE Test_copy (pi_id IN NUMBER) IS
V_REC_Table1 Table1%ROWTYPE;
V_REC_Table2 Table2%ROWTYPE;
V_REC_Table3 Table3%ROWTYPE;
V_REC_Table4 Table4%ROWTYPE;
V_REC_Table5 Table5%ROWTYPE;
V_REC_Table6 Table6%ROWTYPE;
V_REC_Table7 Table7%ROWTYPE;
V_REC_Table8 Table8%ROWTYPE;
V_REC_Table9 Table9%ROWTYPE;
V_REC_Table10 Table10%ROWTYPE;
V_REC_Table1_exists Table1%ROWTYPE;
V_REC_Table2_exists Table2%ROWTYPE;
V_REC_Table3_exists Table3%ROWTYPE;
V_REC_Table4_exists Table4%ROWTYPE;
V_REC_Table5_exists Table5%ROWTYPE;
V_REC_Table6_exists Table6%ROWTYPE;
V_REC_Table7_exists Table7%ROWTYPE;
V_REC_Table8_exists Table8%ROWTYPE;
V_REC_Table9_exists Table9%ROWTYPE;
V_REC_Table10_exists Table10%ROWTYPE;
v_sql VARCHAR2(2000);
Begin
v_sql := 'SELECT * FROM Table1#dblink WHERE ID = '||pi_id;
EXECUTE IMMEDIATE v_sql INTO V_REC_DBLINK_Table1;
Exception
when no_data_found then
Raise_application_Error(-20001, 'ID doesnt exist');
END;
Begin
v_sql := 'SELECT * FROM Table2#dblink WHERE ID = '||V_REC_Table1.T2_ID;
EXECUTE IMMEDIATE v_sql INTO V_REC_DBLINK_Table2;
Exception
when no_data_found then
Raise_application_Error(-20001, 'ID doesnt exist');
END;
(and so on)
.
.
.
BEGIN
v_sql := 'SELECT * FROM Table1 WHERE ID = '||V_REC_DBLINK_TABLE1.ID;
EXECUTE IMMEDIATE v_sql INTO V_REC_DBLINK_TABLE1_EXISTS;
UPDATE TABLE1
SET ID = V_REC_DBLINK_Table1.ID,
NAME = V_REC_DBLINK_Table1.NAME,
DESCRIPTION = V_REC_DBLINK_Table1.DESCRIPTION
NOTE = V_REC_DBLINK_Table1.NOTE
NUMBER = V_REC_DBLINK_Table1.NUMBER
ADDRESS = V_REC_DBLINK_Table1.ADDRESS
WHERE ID = V_REC_DBLINK_TABLE1.ID;
EXCEPTION
when no_data_found then
INSERT INTO TABLE1
(ID, NAME, DESCRIPTION, NOTE, NUMBER, ADDRESS)
Values(V_REC_DBLINK_TABLE1.ID
,V_REC_DBLINK_TABLE1.NAME
,V_REC_DBLINK_TABLE1.DESCRIPTION
,V_REC_DBLINK_TABLE1.NOTE
,V_REC_DBLINK_TABLE1.NUMBER
,V_REC_DBLINK_TABLE1.ADDRESS);
END;
BEGIN
v_sql := 'SELECT * FROM Table2 WHERE ID = '||V_REC_DBLINK_TABLE2.ID;
EXECUTE IMMEDIATE v_sql INTO V_REC_DBLINK_TABLE2_EXISTS;
UPDATE TABLE2
SET ID = V_REC_DBLINK_Table2.ID,
NAME = V_REC_DBLINK_Table2.NAME,
DESCRIPTION = V_REC_DBLINK_Table2.DESCRIPTION
WHERE ID = V_REC_DBLINK_TABLE2.ID;
EXCEPTION
when no_data_found then
INSERT INTO TABLE2
(ID, NAME, DESCRIPTION)
Values(V_REC_DBLINK_TABLE2.ID
,V_REC_DBLINK_TABLE2.NAME
,V_REC_DBLINK_TABLE2.DESCRIPTION);
END;
(and so on for all Tables via DBLINK)
Any suggestions? I just want to do that in a shorter way(maybe via Collection and loop).

Oracle - ORA-38101: Invalid column in the INSERT VALUES Clause on Merge

I'm trying to do an upsert using merge and I'm getting an error message: Oracle - ORA-38101: Invalid column in the INSERT VALUES Clause on Merge: "MY_TABLE"."MYCOL2", even thought the table and column names indicated in the error are correct.
declare
var1 varchar2(50) := 'var1';
var2 varchar2(50) := 'var2';
procedure ins
(mycol1 IN VARCHAR2,
mycol2 IN VARCHAR2)
is
BEGIN
LOOP
BEGIN
MERGE INTO my_table USING dual ON
( MYCOL1 = mycol1
AND MYCOL2 = mycol2
)
WHEN MATCHED THEN UPDATE SET
MYCOL1 = mycol1,
MYCOL2 = mycol2
WHEN NOT MATCHED THEN INSERT
(MYCOL1, MYCOL2)
VALUES ( mycol1, mycol2 );
EXIT; -- success? -> exit loop
EXCEPTION
WHEN NO_DATA_FOUND THEN -- the entry was concurrently deleted
NULL; -- exception? -> no op, i.e. continue looping
WHEN DUP_VAL_ON_INDEX THEN -- an entry was concurrently inserted
NULL; -- exception? -> no op, i.e. continue looping
END;
END LOOP;
END;
begin
ins (var1, var2);
end;
Dont use the variable names which are same as column name of the table.
Your MATCHED clause is doing nothing.
in USING clause, there must be some query as mentioned in the comment.

Get ID of new record in function trigger before insert on postgreSQL

I am making a function trigger that will be executed before insert new record in some table, this function trigger will insert the new record in another table with some values of the first insert. I need to execute before insert because I need the last record in the first table to compare with some fields of the new record.
The problem here is, that I can find how to get the Id of the record that will be inserted cuz the record doesn't exist yet.
I am thinking user a sequence to get the next id that will be generated, but I don't know if these cause problems if there are more than one user writing to the database at the same time.
This is my function PostgreSQL function:
CREATE FUNCTION mydb.mytriggername()
RETURNS trigger
LANGUAGE 'plpgsql'
COST 100.0
VOLATILE NOT LEAKPROOF
AS $BODY$
BEGIN
IF (
SELECT tvalue FROM mydb.myTable WHERE type = NEW.type AND sb = NEW.sb AND st= NEW.st order by id DESC LIMIT 1
) <> NEW.tvalue THEN
INSERT INTO mydb.anotherTable( type, date, desc, tp, sb, st, ref)
SELECT 'tc', NEW.date, 'newdesc', NEW.type, NEW.sb, NEW.st, CONCAT('{"id_ref":', NEW.id, '}');
END IF;
RETURN NEW;
END;
Notice that I need use it on CONCAT('{"id_ref":', NEW.id, '}'); but as I said before, I don't have the id because the new records it doesn't insert yet.
Do you have any approach to solve my problem?
you don't reveal your trigger code, so I'm not sure this is the case, but below is an example of using NEW before INSERT
t=# create table ta1(i int);
CREATE TABLE
t=# create table ta2(i int);
CREATE TABLE
t=# create function taf() returns trigger volatile not leakproof as $$
begin
if NEW.i > 0 then insert into ta2 values (NEW.i); end if;
return NEW;
end;
$$ language plpgsql
;
CREATE FUNCTION
t=# create trigger tg before insert on ta1 for each row execute procedure taf();
CREATE TRIGGER
t=# select * from ta2;
i
---
(0 rows)
t=# begin; insert into ta1 select 5;
BEGIN
INSERT 0 1
t=# select * from ta2;
i
---
5
(1 row)
t=# end;
COMMIT

Atomic Transaction with nested BEFORE INSERT/UPDATE Triggers

Currently i am implementing a procedure, which creates a couple of rows in some related tables out of a template. So my Procedure consists of a SAVEPOINT followed by some INSERT statements on different tables, and a Cursor to insert some more rows to other tables while referencing on the newly created primary keys.
Each of those tables has an BEFORE INSERT/UPDATE trigger defined which has the purpose to:
Get a new primary key from a sequencer if it is not defined in the INSERT statement (there are cases where I need to set the Primary key explicitely to reference it later on in the same transaction)
Set some default values if they are NULL
Set auditing fields (last_change_date, last_change_user, etc..)
The transaction fails with ORA-04091: table is mutating, trigger/function may not see it
I am understanding, that I could Workaround this, by declaring PRAGMA AUTONOMOUS TRANSACTION in each Trigger, but my Transaction would not be atomic any more then, as it is the requirement that all those datasets should be created/inserted as a whole or None of them.
So what am I doing wrong in the design of my database?
UPDATE: This is the Code of the trigger
CREATE TRIGGER TRG_AUFTRAG_B_IU
BEFORE INSERT OR UPDATE
ON AUFTRAG
FOR EACH ROW
BEGIN
IF INSERTING THEN
IF :new.id is NULL or :new.id = 0 THEN
SELECT SEQ_AUFTRAG.nextval into :new.id from dual;
END IF;
IF :new.nummer is NULL or :new.nummer = 0 THEN
SELECT nvl(MAX(NUMMER),0)+1 INTO :new.nummer FROM AUFTRAG WHERE EXTRACT(YEAR from DATUM) = EXTRACT(YEAR from :new.DATUM);
END IF;
--DEFAULT Values
IF :new.BETR_GRENZWERTE_RELEVANT is NULL THEN
SELECT 0 INTO :new.BETR_GRENZWERTE_RELEVANT FROM dual;
END IF;
IF :new.DOKUMENTE_ABGELEGT is NULL THEN
SELECT 0 INTO :new.DOKUMENTE_ABGELEGT FROM dual;
END IF;
IF :new.EXT_ORG is NULL or :new.EXT_ORG < 1 THEN
SELECT 1 INTO :new.EXT_ORG FROM dual;
END IF;
:new.ERSTELLT_VON := nvl(:new.ERSTELLT_VON,user);
:new.ERSTELLT_DATUM := nvl(:new.ERSTELLT_DATUM,sysdate);
END IF;
:new.GEAENDERT_VON := user;
:new.GEAENDERT_DATUM := sysdate;
END;
You can write it more compact like this:
CREATE TRIGGER TRG_AUFTRAG_B_IU
BEFORE INSERT OR UPDATE
ON AUFTRAG
FOR EACH ROW
BEGIN
IF INSERTING THEN
:new.id = NVL(NULLIF(:new.id, 0), SEQ_AUFTRAG.nextval);
--DEFAULT Values
:new.BETR_GRENZWERTE_RELEVANT := NVL(:new.BETR_GRENZWERTE_RELEVANT, 0);
:new.DOKUMENTE_ABGELEGT := NVL(:new.DOKUMENTE_ABGELEGT, 0);
IF :new.EXT_ORG is NULL or :new.EXT_ORG < 1 THEN
:new.EXT_ORG := 1;
END IF;
:new.ERSTELLT_VON := nvl(:new.ERSTELLT_VON,user);
:new.ERSTELLT_DATUM := nvl(:new.ERSTELLT_DATUM,sysdate);
END IF;
:new.GEAENDERT_VON := user;
:new.GEAENDERT_DATUM := sysdate;
END;
Only "problem" is this part
IF :new.nummer is NULL or :new.nummer = 0 THEN
SELECT nvl(MAX(NUMMER),0)+1 INTO :new.nummer
FROM AUFTRAG
WHERE EXTRACT(YEAR from DATUM) = EXTRACT(YEAR from :new.DATUM);
END IF;
This one you should put into your procedure or in a statement trigger (i.e. without FOR EACH ROW clause) like this:
CREATE TRIGGER TRG_AUFTRAG_B_A
AFTER INSERT ON AUFTRAG
BEGIN
UPDATE
(SELECT ID, NUMMER,
ROW_NUMBER() OVER (PARTITION BY EXTRACT(YEAR from DATUM) ORDER BY ID) as N
FROM AUFTRAG)
SET NUMMER = N
WHERE NUMMER IS NULL;
END;

PL/SQL How to convert a record to an assoc array

I am trying to fetch data from a table(employee) with cursor and save to asso. array. However, fetching data with cursor to a record is more straight-forward and it is troublesome to convert a record to an assoc array(arr). Code below is what I am trying to fetch data to a assoc array and improvement is needed. Or any approaches other than cursor? Thank you.
DECLARE
TYPE AssoArray IS TABLE OF varchar2(30) INDEX BY varchar2(30);
arr AssoArray;
table_rec employee%rowtype;
CURSOR cur IS
SELECT * FROM employee;
BEGIN
OPEN cur;
LOOP
FETCH cur into table_rec;
EXIT WHEN cur%notfound;
-- how to improve the code in the section below,
arr('col1') := table_rec.col1;
arr('col2') := table_rec.col2;
arr('col3') := table_rec.col3;
...
arr('col50') := table_rec.col50;
-- end of section
-- do sth
END LOOP;
END;
Could you explain what do you want to get. But I see in your code arr AssoArray will contain only one last fetched row and will be rewrite each cycle. Are you realy need it? My suggestion is you want to get some rows of table as associated array. If its true, you may create the array as table of rowtype and (for example) indexed it by id(if you ID-s is integer).
TYPE AssoArray IS TABLE OF employee%rowtype INDEX BY PLS_INTEGER;
For example
create table tmp_table_01 as select * from all_objects where rownum < 11;
DECLARE
TYPE AssoArray IS TABLE OF tmp_table_01%rowtype INDEX BY varchar2(30);
arr AssoArray;
table_rec tmp_table_01%rowtype;
CURSOR cur IS
SELECT * FROM all_objects where rownum < 20;
BEGIN
OPEN cur;
LOOP
FETCH cur into table_rec;
EXIT WHEN cur%notfound;
-- how to improve the code in the section below,
arr(table_rec.object_name) := table_rec;
-- end of section
dbms_output.put_line(table_rec.object_name ||' '||arr(table_rec.object_name).object_id );
-- do sth
END LOOP;
END;
EDIT:
If you want to make comraisons by table structure you may use Dynamic SQL. NExt code get data from table TMP_TABLE_01 sort it by object_id, compare neighbour rows, and return count of difference row.
DECLARE
l_sql varchar2(32767);
TYPE t_cols_type IS table of varchar2(30);
l_cols t_cols_type ;
l_result number;
BEGIN
SELECT c.COLUMN_NAME
BULK COLLECt INTO l_cols
FROM user_tab_cols c
WHERE c.TABLE_NAME = 'TMP_TABLE_01';
l_sql := 'WITH s AS (SELECT t.*, row_number() OVER (ORDER BY object_id) AS rn FROM TMP_TABLE_01 t)
SELECT count(*)
FROM s
JOIN s sub_s ON s.rn = sub_s.rn - 1
where 1=0 ';
FOR i IN 1 .. l_cols.last LOOP
l_sql := l_sql || ' OR decode(s.'||l_cols(i)||',sub_s.'||l_cols(i)||',1,0) = 0';
END LOOP;
dbms_output.put_line(l_sql);
EXECUTE IMMEDIATE l_sql
INTO l_result ;
dbms_output.put_line('count of conseuence different rows ' ||l_result);
END;

Resources