How To Create a PL/SQL Trigger That Detects an Inserted or Updated Row and updates a Record in a Different Table? - plsql

I am creating a book tracking database for myself that holds information about my books and allows me to keep track of who is borrowing them. I am trying to create a trigger on my Checkouts table that runs if a record is added or updated that will determine if a checkout data has been entered or if a checkin date has been entered and change the "available" field in my Books table to "Y" or "N".
I have created a trigger called "update_book_availablility" on my Checkouts table but I keep getting this error:
"PLS-00103: Encountered the symbol 'end-of-file' when expecting one of the following: ( begin case declare and exception exit for goto if loop mod null pragma raise return select update while with <<continue close current delete fetch lock insert open rollback savepoint set sql execute commit forall merge standard pipe purge json_object
Errors: check compiler log"
Here is my trigger code:
CREATE OR REPLACE NONEDITIONABLE TRIGGER "UPDATE_BOOK_AVAILABILITY"
AFTER INSERT OR UPDATE OF ISBN, PersonID, checkout_date, checkin_date
ON Checkouts
FOR EACH ROW
BEGIN
IF :NEW.checkout_date = NULL
THEN
UPDATE Book
SET available = 'N'
WHERE ISBN IN (SELECT :NEW.ISBN FROM Checkouts);
END IF;
END;
Here is an image of my ERD:
ERD
I have been looking into and double checking my trigger syntax, If condition syntax, subquery syntax, and googling this error but have found nothing that has helped. I am new to PL/SQL and would appreciate any help in understanding what I have done wrong or missed.

PLS-00103: Encountered the symbol end-of-file error is SYNTAX ERROR
Copied your trigger and adjusted it to one of my test tables - it works. I removed NONEDITIONABLE and changed trigger table name as well as column names and table/column beeing updated by trigger.
To Do:
Check your syntax again or write the trigger from scratch once more
"...WHERE ISBN IN (SELECT :NEW.ISBN FROM Checkouts)..." selects one fixed value (FOR EACH ROW) :NEW.ISBN of triggering table, better ->> "... WHERE ISBN = :NEW.ISBN ..."
Prety sure that you don't need NONEDITIONABLE trigger for your books tracking app...
Regards...

Related

SQLite error: cannot start a transaction within a transaction with very basic tables

I am brand new to SQL, and I am learning on an SQLite editor. So I create a couple of very simple tables. This code is straight from Linkedin learning "SQL essential training", and I am using the recommended SQLite editor.
CREATE TABLE widgetInventory(
id INTEGER PRIMARY KEY,
description TEXT,
onhand INTEGER NOT NULL);
CREATE TABLE widgetSales(
id INTEGER PRIMARY KEY,
inv_id INTEGER,
quan INTEGER,
price INTEGER);
Then I update widgetInventory with some data:
INSERT INTO widgetInventory (description, onhand) VALUES ('rock', 25);
INSERT INTO widgetInventory (description, onhand) VALUES ('paper', 25);
INSERT INTO widgetInventory (description, onhand) VALUES ('scissors', 25);
Next, I want to update the widgetSales table with a sale, and update the widgetInventory table to record the reduction of onhand.
BEGIN TRANSACTION;
INSERT INTO widgetSales (inv_id, quan, price) VALUES (1,5,500);
UPDATE widgetInventory SET onhand = (onhand-5) WHERE id = 1;
END TRANSACTION;
I am not understanding why this gives me an error when I run it, as it is exactly as it is in the lesson.
[06:18:04] Error while executing SQL query on database 'test': cannot start a transaction within a transaction
But, I can run the INSERT and UPDATE lines separately, and they do what I want them to do.
Apparently, running - END TRANSACTION; - before running the entire transaction appears to work.
I think that somehow, SQL thinks that a transaction is already occurring. Though, I'm not sure where exactly. So to stop it, you have to end the transaction first before proceeding with the course.
In the SQLite Editor, you may have to delete or comment out all of the code before and after these two transactions.
BEGIN TRANSACTION;
INSERT INTO widgetSales ( inv_id, quan, price ) VALUES ( 1, 5, 500 );
UPDATE widgetInventory SET onhand = ( onhand - 5 ) WHERE id = 1;
END TRANSACTION;
BEGIN TRANSACTION;
INSERT INTO widgetInventory ( description, onhand ) VALUES ( 'toy', 25 );
ROLLBACK;
Otherwise it won't execute the transaction.
Other than that, there is probably an error written in somewhere. Copying and pasting in the .txt file didn't give me that transaction error and could execute the transaction normally.
Just had this same error and my issue was I only highlighted the first line so SQLLite started the transaction but didn't run it fully. All I did was run end transaction, highlight the whole block of code and run that and it worked fine. Must be some syntax issue in Lite that doesn't run the full block itself.
while executing SQL query on database 'test': cannot start a
transaction within a transaction
means a transaction already exists. It may happen if someone forgets to select the END TRANSACTION; statement.
If you face this issue just select END TRANSACTION once and run. With this it will end the active transaction and then you can run any of the existing transaction.
For the particular case of following the Linkedin learning "SQL essential training" course, I have figured out to fix it by running (f9) the "BEGIN TRANSACTION", "...TRANSACTION CONTENTS..." and "END TRANSACTION" statements separately, not all the statements at the same time.
So,
First select the "BEGIN TRANSACTION;" and run it by pressing f9.
Then select the contents of the transactions (I think you can include also the "END TRANSACTION;" part) and run it.

Table mutating when "after insert" trigger fired, i don't know what to do

I want to increment the samenameamount column in my table employees after inserting the similar name
I have a such trigger:
create or replace trigger countNumbEmp after insert on employees for each row
declare
var_count_names number;--var for count same names
begin
select count(emp_name) into var_count_names from employees where emp_name = :new.emp_name;
update employees set samenameamount = var_count_names where emp_name = :new.emp_name;
end;
My table is
- Name Null? Type
-------------- ----- ------------
EMP_ID NUMBER
EMP_NAME VARCHAR2(20)
SAMENAMEAMOUNT NUMBER
After inserting i get this message:
- Error report -
ORA-04091: table DASTAN.EMPLOYEES is mutating, trigger/function may not see it
ORA-06512: at "DASTAN.COUNTNUMBEMP", line 4
ORA-04088: error during execution of trigger 'DASTAN.COUNTNUMBEMP'
Here's a brief list of things that you should not do in a trigger (source link here, my personal favorite page):
On insert triggers have no :OLD values.
On delete triggers have no :NEW values.
Triggers do not commit transactions. If a transaction is rolled back,
the data changed by the trigger is also rolled back.
Commits, rollbacks and save points are not allowed in the trigger
body. A commit/rollback affects the entire transaction, it is all or
none.
Unhandled exceptions in the trigger will cause a rollback of the
entire transaction, not just the trigger.
If more than one trigger is defined on an event, the order in which
they fire is not defined. If the triggers must fire in order, you
must create one trigger that executes all the actions in the required
order.
A trigger can cause other events to execute triggers. A trigger can
not change a table that it has read from. This is the mutating table
error issue.
I am intentionally not pasting the last part of that link, because it mentions things like "how to commit in a trigger" etc., as it's a bad practice, even though it IS in fact a workaround. If you can't avoid having that, however, there's something wrong with the design of the solution or the implementation and this needs to be solved first.
Cheers

sqlite trigger causes "no such column" exception

I'm a newbie with sql triggers and am getting an ESQLiteException on what seems like a simple example. When I try to modify the "memberTag" column in an existing row, I get the exception "no such column: memberTag". If I drop the trigger, the exception goes away and the row gets updated.
I'm using SQLite and I'm using the "SQLite Expert Personal" app to do this experimenting.
I have this table:
CREATE TABLE [znode] (
[description] CHAR NOT NULL,
[memberTag] CHAR);
and this trigger:
CREATE TRIGGER [memberTagTrigger]
AFTER UPDATE
ON [znode]
FOR EACH ROW
WHEN length(memberTag)=0
BEGIN
update znode
set memberTag = null;
END;
My update experiment data is something like this:
description memberTag
one x
two (null)
And when I try to change (null) to "y" using SQLite Expert Personal, it throws the exception.
The problem is in the WHEN clause: the database does not know where memberTag comes from, because there are two possible rows, the old one, and the new one.
Use either OLD.memberTag or NEW.memberTag.
(There is another problem: the UPDATE will change all rows in the table, because you forgot the WHERE clause.)

trigger for updating a value

I am a newbie in PLSQL and I would like to create a trigger that checks first if there is a record in a table before making an update.
The code I got so far is:
CREATE OR REPLACE TRIGGER table_bu
BEFORE UPDATE ON employee
FOR EACH ROW
DECLARE
v_employee_id:=employee.employee_ID%TYPE;
BEGIN
SELECT employee_id INTO v_employee_id FROM employee;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR (-20001,'data not found');
END;
How I can create a trigger that checks up if a record exists in the table and if it does not exists does not allow the update.
My table estructure is:
employee_id NUMBER
employee_name VARCHAR(20)
employee_salary NUMBER
...
Thanks
You are on a wrong way. The trigger as it is will throw runtime 'Mutating table' error even after fixing syntax error - you missed semicolon after raise_application_error(also it should take 2 arguments, not one). Correct syntax :
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR (-20001, 'data not found'); -- 1st parameter -error code
Update
As far as I understand the updated version of the question, you want to show error if record doesn't exist. The problem with row level trigger approach is that it won't be executed if nothing is found due to condition in WHERE. The simplest way is to check number of rows affected on client side and raise an error there. Or you can write a procedure that checks sql%rowcount after executing desired update, and then throw an exception if it's 0.
If you prefer to do in a hard way, you can create package variable which of type employee.employee_ID%TYPE, before update statement level trigger that resets variable (say set it to null), after update row level trigger that sets this variable to NEW.employee_ID, and after update statement level trigger that throws an exception if the variable is null. Note: this will properly work for individual updates only.
"How I can create a trigger that checks up if a record exists in the table and if it does not exists does not allow the update."
There is really only one practical way to do this - use a referential constraint (foreign key).

How to find out which package/procedure is updating a table?

I would like to find out if it is possible to find out which package or procedure in a package is updating a table?
Due to a certain project being handed over (the person who handed over the project has since left) without proper documentation, data that we know we have updated always go back to some strange source point.
We are guessing that this could be a database job or scheduler that is running the update command without our knowledge. I am hoping that there is a way to find out where the source code is calling from that is updating the table and inserting the source as a trigger on that table that we are monitoring.
Any ideas?
Thanks.
UPDATE: I poked around and found out
how to trace a statement back to its
owning PL/SQL object.
In combination with what Tony mentioned, you can create a logging table and a trigger that looks like this:
CREATE TABLE statement_tracker
( SID NUMBER
, serial# NUMBER
, date_run DATE
, program VARCHAR2(48) null
, module VARCHAR2(48) null
, machine VARCHAR2(64) null
, osuser VARCHAR2(30) null
, sql_text CLOB null
, program_id number
);
CREATE OR REPLACE TRIGGER smb_t_t
AFTER UPDATE
ON smb_test
BEGIN
INSERT
INTO statement_tracker
SELECT ss.SID
, ss.serial#
, sysdate
, ss.program
, ss.module
, ss.machine
, ss.osuser
, sq.sql_fulltext
, sq.program_id
FROM v$session ss
, v$sql sq
WHERE ss.sql_address = sq.address
AND ss.SID = USERENV('sid');
END;
/
In order for the trigger above to compile, you'll need to grant the owner of the trigger these permissions, when logged in as the SYS user:
grant select on V_$SESSION to <user>;
grant select on V_$SQL to <user>;
You will likely want to protect the insert statement in the trigger with some condition that only makes it log when the the change you're interested in is occurring - on my test server this statement runs rather slowly (1 second), so I wouldn't want to be logging all these updates. Of course, in that case, you'd need to change the trigger to be a row-level one so that you could inspect the :new or :old values. If you are really concerned about the overhead of the select, you can change it to not join against v$sql, and instead just save the SQL_ADDRESS column, then schedule a job with DBMS_JOB to go off and update the sql_text column with a second update statement, thereby offloading the update into another session and not blocking your original update.
Unfortunately, this will only tell you half the story. The statement you're going to see logged is going to be the most proximal statement - in this case, an update - even if the original statement executed by the process that initiated it is a stored procedure. This is where the program_id column comes in. If the update statement is part of a procedure or trigger, program_id will point to the object_id of the code in question - you can resolve it thusly:
SELECT * FROM all_objects where object_id = <program_id>;
In the case when the update statement was executed directly from the client, I don't know what program_id represents, but you wouldn't need it - you'd have the name of the executable in the "program" column of statement_tracker. If the update was executed from an anonymous PL/SQL block, I'm not how to track it back - you'll need to experiment further.
It may be, though, that the osuser/machine/program/module information may be enough to get you pointed in the right direction.
If it is a scheduled database job then you can find out what scheduled database jobs exist and look into what they do. Other things you can do are:
look at the dependencies views e.g. ALL_DEPENDENCIES to see what packages/triggers etc. use that table. Depending on the size of your system that may return a lot of objects to trawl through.
Search all the database source code for references to the table like this:
select distinct type, name
from all_source
where lower(text) like lower('%mytable%');
Again that may return a lot of objects, and of course there will be some "false positives" where the search string appears but isn't actually a reference to that table. You could even try something more specific like:
select distinct type, name
from all_source
where lower(text) like lower('%insert into mytable%');
but of course that would miss cases where the command was formatted differently.
Additionally, could there be SQL scripts being run through "cron" jobs on the server?
Just write an "after update" trigger and, in this trigger, log the results of "DBMS_UTILITY.FORMAT_CALL_STACK" in a dedicated table.
The purpose of this function is exactly to give you the complete call stack of al the stored procedures and triggers that have been fired to reach your code.
I am writing from the mobile app, so i can't give you more detailed examples, but if you google for it you'll find many of them.
A quick and dirty option if you're working locally, and are only interested in the first thing that's altering the data, is to throw an error in the trigger instead of logging. That way, you get the usual stack trace and it's a lot less typing and you don't need to create a new table:
AFTER UPDATE ON table_of_interest
BEGIN
RAISE_APPLICATION_ERROR(-20001, 'something changed it');
END;
/

Resources