Oracle - Is there any way I can get around SELECT INTO on PL/SQL block - sql

I am trying to create a trigger, which automatically updates a student's application state when the application status row in the application table changes. I have been browsing the web for a little over an hour or so now and despite finding a potential work around using EXECUTE IMMEDIATE I cannot achieve my desired result (EXECUTE IMMEDIATE was causing an unbound variable error).
Trigger code
CREATE OR REPLACE TRIGGER trg_applications
BEFORE INSERT OR UPDATE ON applications FOR EACH ROW
BEGIN
IF UPDATING THEN
/* If the status is ACCEPTED, then approve the students application */
SELECT CASE
WHEN get_status(:NEW.status_id) =
LOWER('Applicant Accepted Offer')
THEN student_accept_offer( :NEW.student_id )
END
FROM status;
END IF;
END;
The get status method returns a VARCHAR2 to check whether the new status matches the condition, if so I want to update the student_approved row using the autonomous_transaction below.
student_accept_offer code
CREATE OR REPLACE FUNCTION student_accept_offer( this_stu_id NUMBER )
RETURN VARCHAR2 IS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
UPDATE students
SET students.student_on_placement = 1
WHERE students.student_id = this_stu_id;
COMMIT;
RETURN 'Student has approved application';
END student_accept_offer;
This function works as intended when I test it outside of my trigger, however when it is embedded in the trigger an PLS-00428 error gets thrown. Could anyone point me in the right direction as to how can I work around this, to allow me to have this function fire automatically on an update if the status matches.
Thanks for your time
EDIT - Tables I am referencing

Changing your code slightly to remove the SELECT statement (as it seems unnecessary) then does this work?
CREATE OR REPLACE TRIGGER trg_applications
BEFORE INSERT OR UPDATE ON applications FOR EACH ROW
BEGIN
IF UPDATING THEN
/* If the status is ACCEPTED, then approve the students application */
IF get_status(:NEW.status_id) = 'applicant accepted offer' THEN
student_accept_offer( :NEW.student_id );
END IF;
END IF;
END;

Related

delete statement not deleting records

Anyone know why its not deleting the records from the student table. When the table is empty and i first run the anonymous block it runs fine, but then when i run it again i get errors about duplicate primary keys, but this shouldn't matter as each time i run the block it should delete all records from the table?? I'm relatively new to SQL so any help is appreciated.
I should add, the procedure seems to work fine as the dopl appears when i run the code.
This is my function:
CREATE OR REPLACE FUNCTION DELETE_ALL_STUDENTS RETURN NUMBER AS
BEGIN
DELETE FROM STUDENTS;
END;
and this is my procedure:
create or replace PROCEDURE DELETE_ALL_STUDENTS_VIASQLDEV AS
BEGIN
dbms_output.put_line('--------------------------------------------');
dbms_output.put_line('Deleting all student rows');
END;
and this is the anonymous block i was running to see if it worked:
begin
DELETE_ALL_STUDENTS_VIASQLDEV;
ADD_STUDENT_VIASQLDEV(1,'Fred Smith');
ADD_STUDENT_VIASQLDEV(2,'Sue Davis');
ADD_STUDENT_VIASQLDEV(3,'Emma Jones');
end;
"doesnt my function carry out the deleting?"
It would if you called it, but alas you don't.
This is not a SQL problem, it's a logic problem. If we don't do the washing up the dishes remain dirty. Similarly if you don't call the routine which deletes the records the records are not deleted.
You need to call the function in the procedure. Not sure why you've made it a function, and it won't compile anyway, because it doesn't have a RETURN clause. So, let's fix that too.
CREATE OR REPLACE FUNCTION DELETE_ALL_STUDENTS RETURN NUMBER AS
BEGIN
DELETE FROM STUDENTS;
return sql%rowcount; -- how many rows were deleted
END;
/
Now we call it:
create or replace PROCEDURE DELETE_ALL_STUDENTS_VIASQLDEV AS
n number;
BEGIN
dbms_output.put_line('--------------------------------------------');
dbms_output.put_line('Deleting all student rows');
n := DELETE_ALL_STUDENTS;
dbms_output.put_line('No of students deleted = '|| to_char(n));
END;
So, when you run your anonymous block the existing students will be deleted and replaced with the new ones.

Count(*) not working properly

I create the trigger A1 so that an article with a certain type, that is 'Bert' cannot be added more than once and it can have only 1 in the stock.
However, although i create the trigger, i can still add an article with the type 'Bert'. Somehow, the count returns '0' but when i run the same sql statement, it returns the correct number. It also starts counting properly if I drop the trigger and re-add it. Any ideas what might be going wrong?
TRIGGER A1 BEFORE INSERT ON mytable
FOR EACH ROW
DECLARE
l_count NUMBER;
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
SELECT COUNT(*) INTO l_count FROM mytable WHERE article = :new.article;
dbms_output.put_line('Count: ' || l_count);
IF l_count >0 THEN
IF(:new.TYPEB = 'Bert') THEN
dbms_output.put_line('article already exists!');
ROLLBACK;
END IF;
ELSIF (:new.TYPEB = 'Bert' AND :new.stock_count>1) THEN
dbms_output.put_line('stock cannot have more than 1 of this article with type Bert');
ROLLBACK;
END IF;
END;
This is the insert statement I use:
INSERT INTO mytable VALUES('Chip',1,9,1,'Bert');
A couple of points. First, you are misusing the autonomous transaction pragma. It is meant for separate transactions you need to commit or rollback independently of the main transaction. You are using it to rollback the main transaction -- and you never commit if there is no error.
And those "unforeseen consequences" someone mentioned? One of them is that your count always returns 0. So remove the pragma both because it is being misused and so the count will return a proper value.
Another thing is don't have commits or rollbacks within triggers. Raise an error and let the controlling code do what it needs to do. I know the rollbacks were because of the pragma. Just don't forget to remove them when you remove the pragma.
The following trigger works for me:
CREATE OR REPLACE TRIGGER trg_mytable_biu
BEFORE INSERT OR UPDATE ON mytable
FOR EACH ROW
WHEN (NEW.TYPEB = 'Bert') -- Don't even execute unless this is Bert
DECLARE
L_COUNT NUMBER;
BEGIN
SELECT COUNT(*) INTO L_COUNT
FROM MYTABLE
WHERE ARTICLE = :NEW.ARTICLE
AND TYPEB = :NEW.TYPEB;
IF L_COUNT > 0 THEN
RAISE_APPLICATION_ERROR( -20001, 'Bert already exists!' );
ELSIF :NEW.STOCK_COUNT > 1 THEN
RAISE_APPLICATION_ERROR( -20001, 'Can''t insert more than one Bert!' );
END IF;
END;
However, it's not a good idea for a trigger on a table to separately access that table. Usually the system won't even allow it -- this trigger won't execute at all if changed to "after". If it is allowed to execute, one can never be sure of the results obtained -- as you already found out. Actually, I'm a little surprised the trigger above works. I would feel uneasy using it in a real database.
The best option when a trigger must access the target table is to hide the table behind a view and write an "instead of" trigger on the view. That trigger can access the table all it wants.
You need to do an AFTER trigger, not a BEFORE trigger. Doing a count(*) "BEFORE" the insert occurs results in zero rows because the data hasn't been inserted yet.

Writing an SQL trigger to find if number appears in column more than X times?

I want to write a Postgres SQL trigger that will basically find if a number appears in a column 5 or more times. If it appears a 5th time, I want to throw an exception. Here is how the table looks:
create table tab(
first integer not null constraint pk_part_id primary key,
second integer constraint fk_super_part_id references bom,
price integer);
insert into tab values(1,NULL,100), (2,1,50), (3,1,30), (4,2,20), (5,2,10), (6,3,20);
Above are the original inserts into the table. My trigger will occur upon inserting more values into the table.
Basically if a number appears in the 'second' column more than 4 times after inserting into the table, I want to raise an exception. Here is my attempt at writing the trigger:
create function check() return trigger as '
begin
if(select first, second, price
from tab
where second in (
select second from tab
group by second
having count(second) > 4)
) then
raise exception ''Error, there are more than 5 parts.'';
end if;
return null;
end
'language plpgsql;
create trigger check
after insert or update on tab
for each row execute procedure check();
Could anyone help me out? If so that would be great! Thanks!
CREATE FUNCTION trg_upbef()
RETURN trigger as
$func$
BEGIN
IF (SELECT count(*)
FROM tab
WHERE second = NEW.second ) > 3 THEN
RAISE EXCEPTION 'Error: there are more than 5 parts.';
END IF;
RETURN NEW; -- must be NEW for BEFORE trigger
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER upbef
BEFORE INSERT OR UPDATE ON tab
FOR EACH ROW EXECUTE procedure trg_upbef();
Major points
Keyword is RETURNS, not RETURN.
Use the special variable NEW to refer to the newly inserted / updated row.
Use a BEFORE trigger. Better skip early in case of an exception.
Don't count everything for your test, just what you need. Much faster.
Use dollar-quoting. Makes your live easier.
Concurrency:
If you want to be absolutely sure, you'll have to take an exclusive lock on the table before counting. Else, concurrent inserts / updates might outfox each other under heavy concurrent load. While this is rather unlikely, it's possible.

What is wrong with my Oracle Trigger?

CREATE OR REPLACE TRIGGER Net_winnings_trigger
AFTER UPDATE OF total_winnings ON Players
FOR EACH ROW
DECLARE
OldTuple OLD
NewTuple NEW
BEGIN
IF(OldTuple.total_winnings > NewTuple.total_winnings)
THEN
UPDATE Players
SET total_winnings = OldTuple.total_winnings
WHERE player_no = NewTuple.player_no;
END IF;
END;
/
I am trying to get a trigger that will only allow the 'total_winnings' field to be updated to a value greater than the current value.
If an update to a smaller value occurs, the trigger should just leave the set the value to the old value (as if the update never occured)
Since you want to override the value that is specified in the UPDATE statement, you'd need to use a BEFORE UPDATE trigger. Something like this
CREATE OR REPLACE TRIGGER Net_winnings_trigger
BEFORE UPDATE OF total_winnings ON Players
FOR EACH ROW
BEGIN
IF(:old.total_winnings > :new.total_winnings)
THEN
:new.total_winnings := :old.total_winnings;
END IF;
END;
But overriding the value specified in an UPDATE statement is a dangerous game. If this is something that shouldn't happen, you really ought to raise an error so that the application can be made aware that there was a problem. Otherwise, you're creating all sorts of potential for the application to make incorrect decisions down the line.
Something like this should work.. although it will be hiding the fact that an update is not taking place if you try to update to a smaller value. To the user, everything will look like it worked but the data will remain unchanged.
CREATE OR REPLACE TRIGGER Net_winnings_trigger
BEFORE UPDATE OF total_winnings
ON Players
FOR EACH ROW
DECLARE
BEGIN
:new.total_winnings := greatest(:old.total_winnings,:new.total_winnings);
END;

Postgres: checking value before conditionally running an update or delete

I've got a fairly simple table which stores the records' authors in a text field as shown here:
CREATE TABLE "public"."test_tbl" (
"index" SERIAL,
"testdate" DATE,
"pfr_author" TEXT DEFAULT "current_user"(),
CONSTRAINT "test_tbl_pkey" PRIMARY KEY("index");
The user will never see the index or pfr_author fields, but I'd like them to be able to UPDATE the testdate field or DELETE whole records if they have permission and if they are the author. i.e. if test_tbl.pfr_author = CURRENT_USER THEN permit the UPDATE OR DELETE, but if not then raise an error message such as "Sorry, you do not have permission to edit this record.".
I have not gone down the route of using a trigger as I figure that even if it is executed before row update the user-requested update will still take place afterwards regardless.
I've tried doing this through a rule, but end up with infinite recursion as I put an update command inside the rule. Is there some way to do this using rules alone or a combination of a rule and trigger?
Thanks very much for any help!
Use a row level BEFORE trigger on UPDATE and DELETE to do this. Just have it return NULL when the operation is not permitted and the operation will be skipped.
http://www.postgresql.org/docs/9.0/interactive/trigger-definition.html
the trigger function have some problem,resulting recursive loop update.You should do like this:
CREATE OR REPLACE FUNCTION "public"."test_tbl_trig_func" () RETURNS trigger AS $body$
BEGIN
IF not (old.pfr_author = "current_user"() OR "current_user"() = 'postgres') THEN
NULL;
END IF;
RETURN new;
END;
$body$ LANGUAGE 'plpgsql' VOLATILE CALLED ON NULL INPUT SECURITY INVOKER COST 100;
I have a test like this,it does well;
UPDATE test_tbl SET testdate = CURRENT_DATE WHERE test_tbl."index" = 2;