mutating table with this trigger oracle database - sql

Use Oracle database and need to store all rows in lower case. All INSERTand UPDATE should insert and update rows with lower case data. I wrote a trigger to do it. Read a lot of about table mutation with triggers.Very confused, will this code raise mutate error or not. Please, write here, if it will.
create or replace trigger employee_name
before update or insert on employee
for each row
begin
/* convert character values to lower case */
:new.lastname := lower( :new.lastname );
:new.firstname :=lower( :new.firstname );
end;
/

The trigger will not raise a mutating table error as you're not selecting from the table your trigger is on; you're only reassigning values, which is what triggers are designed to do.
It's worth noting that this can also be accomplished with a CHECK constraint, which will force everyone updating or inserting into the table to lower case everything:
alter table employee_name
add constraint chk_emp_name_lastname
check ( lastname = lower(lastname) )
Whilst this will increase update/insert times (not necessarily more than using a trigger) it doesn't silently change data being entered but instead complains that the data is incorrect. This is sometimes a better approach.

this code not raise exception
because you not try select from table which trigger executing

This should not raise any errors. Mutate errors occur when you read (or modify) some other rows from BEFORE trigger.
NOTE: there are situations where BEFORE trigger is fired more that once for the SAME row.
In order to guarantee consistency Oracle would have to justify, that your trigger body is "idempotent": e.i. each execution on the same input gives the same result.

Related

Oracle Triggers, reliant on a key generated by a second trigger

I have question about triggers. For my database assignment, I have to create a trigger that automatically generates an ID number, which I have done.
The problem is a second trigger needs to be made that also acts on the same data. Both are inserts. Since they have to be separate, I am not sure how to make this work.
From what I have been taught the way to make a trigger act on the most recent addition is to use:
WHERE ID=:NEW.ID;
where ID is the primary key, but this does not work for me when the ID is being generated by a trigger. Is there a method of creating a trigger that acts on the most recent row added to the table that does not reference the primary key?
It is not clear why you need two triggers but let's assume it really makes sense (which I doubt).
Does the first trigger generates the ID from a sequence? In this case you can use value CURRVAL of the sequence. This pseudocolumn returns the current value of a sequence without increasing the value, see Sequence Pseudocolumns
If this does not fit your needs you can write a procedure which is then called be the trigger.
Would be like this:
create procedure PROC(aRow in ROWID) as
begin
...
end;
create first_trigger ....
begin
... whatever is needed at first trigger.
PROC(NEW.ROWID);
end;
create second_trigger ....
begin
... whatever is needed at second trigger.
PROC(NEW.ROWID);
end;
Both triggers would operate on the same row. You can also write current row values into a PL/SQL variable and process them by a Statement-Trigger (i.e. no row-level-trigger)

Can you easily and efficiently copy or edit the INSERTED table in a trigger?

I'm writing a trigger in which I need to check the incoming data and potentially change it. Then later in the trigger I need to use that new data for further processing. A highly simplified version looks something like this:
ALTER TRIGGER [db].[trig_update]
ON [db].[table]
AFTER UPDATE
AS
BEGIN
DECLARE #thisprofileID int
IF (Inserted.profileID IS NULL)
BEGIN
SELECT #thisprofileID=profileID
FROM db.otherTable
WHERE userid = #thisuserID;
UPDATE db.table
SET profileID = #thisprofileID
WHERE userid = #thisuserID;
-- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
-- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
END
IF ({conditional})
BEGIN
UPDATE db.thirdTable
SET [profileID] = Inserted.profileID
...{20+ other fields}
FROM Inserted ...{a few joins}
WHERE {various criteria}
END
END
The problem that we're running into is that the update statement fails because Inserted.profileID is null, and thirdTable.profileID is set to not allow nulls. table.profileID will never stay null; if it is created as null then this trigger should catch it and set it to a value. But even though we're updated 'table', Inserted still has the null value. So far it makes sense to me why this is happening.
I'm unsure how to correct the problem. In the area with commented Xs I tried running an update query against the Inserted table to update profileID, but this resulted in an error because the pseudo-table apparently can't be updated. Am I incorrect in this presumption? That would be an easy solution.
The next most logical solution to me would be to INSERT INTO a table variable to make a copy of Inserted and then use that in the rest of the trigger, but that fails because the table variable is not defined. Defining that table variable would require more fields than I care to count, and will present a major maintenance nightmare any time that we need to make changes to the structure of 'table'. So assuming this is the best approach, is there an easy way to copy the data and structure of Inserted into a table variable without explicitly defining the structure?
I don't think that a temp table (which I could otherwise easily insert into) would be a good solution because my limited understanding is that they are far slower than a table variable that lives only inside the trigger. I assume that temp table also must be public, and cause problems if our trigger fires twice and both instances need the temp table.

Can dynamic SQL be called from a trigger in Oracle?

I have a dozen tables of whom I want to keep the history of the changes. For every one I created a second table with the ending _HISTO and added fields modtime, action, user.
At the moment before I insert, modify or delete a record in this tables I call ( from my delphi app ) a oracle procedure that copies the actual values to the histo table and then do the operation.
My procedure generates a dynamic sql via DBA_TAB_COLUMNS and then executes the generated ( insert into tablename_histo ( fields s ) select fields, sysdate, 'acition', userid from table_name
I was told that I can not call this procedure from a trigger because it has to select the table the trigger is triggered on. Is this true ? Is it possible to implement what I need ?
Assuming you want to maintain history using triggers (rather than any of the other methods of tracking history data in Oracle-- Workspace Manager, Total Recall, Streams, Fine_Grained Auditing etc.), you can use dynamic SQL in the trigger. But the dynamic SQL is subject to the same rules that static SQL is subject to. And even static SQL in a row-level trigger cannot in general query the table that the trigger is defined on without generating a mutating table exception.
Rather than calling dynamic SQL from your trigger, however, you can potentially write some dynamic SQL that generates the trigger in the first place using the same data dictionary tables. The triggers themselves would statically refer to :new.column_name and :old.column_name. Of course, you would have to either edit the trigger or re-run the procedure that dynamically creates the trigger when a new column gets added. Since you, presumably, need to add the column to both the main table and the history table, however, this generally isn't too big of a deal.
Oracle does not allow a trigger to execute a SELECT against the table on which the trigger is defined. If you try it you'll get the dreaded "mutating table" error (ORA-04091), and while there are ways to get around that error they add a lot of complexity for little value. If you really want to build a dynamic query every time your table is updated (IMO this is a bad idea from the standpoint of performance - I find that metadata queries are often slow, but YMMV) it should end up looking something like
strAction := CASE
WHEN INSERTING THEN 'INSERT'
WHEN UPDATING THEN 'UPDATE'
WHEN DELETING THEN 'DELETE'
END;
INSERT INTO TABLENAME_HISTO
(ACTIVITY_DATE, ACTION, MTC_USER,
old_field1, new_field1, old_field2, new_field2)
VALUES
(SYSDATE, strAction, USERID,
:OLD.field1, :NEW.field1, :OLD.field2, :NEW.field2)
Share and enjoy.

Oracle - Problem creating trigger that updates another table

I've read the Oracle docs on creating triggers and am doing things exactly how it shows, however this just isn't working. My goal is to update the TPM_PROJECT table with the minimum STARTDATE appearing in the TPM_TRAININGPLAN table. Thus, every time someone updates the STARTDATE column in TPM_TRAININGPLAN, I want to update teh TPM_PROJECT table. Here's what I'm trying:
CREATE TRIGGER Trigger_UpdateTrainingDelivery
AFTER DELETE OR INSERT OR UPDATE OF STARTDATE
ON TPM_TRAININGPLAN
FOR EACH ROW WHEN (new.TRAININGPLANTYPE='prescribed')
BEGIN
UPDATE TPM_PROJECT SET TRAININGDELIVERYSTART = (SELECT MIN(TP.STARTDATE) FROM TPM_TRAININGPLAN TP WHERE TP.PROJECTID = new.PROJECTID AND TP.TRAININGPLANTYPE='prescribed')
WHERE PROJECTID = new.PROJECTID
END;
The trigger is created with no errors, but I do get a warning:
Warnings: --->
W (1): Warning: execution completed with warning
<---
Of course Oracle isn't nice enough to actually tell me what the warning is, I simply am shown that there is one.
Next, if I update the training plan table with:
UPDATE TPM_TRAININGPLAN
set STARTDATE = to_date('03/12/2009','mm/dd/yyyy')
where TRAININGPLANID=15916;
I get the error message:
>[Error] Script lines: 20-22 ------------------------
ORA-04098: trigger 'TPMDBO.TRIGGER_UPDATETRAININGDELIVERY' is invalid and failed re-validation
Script line 20, statement line 1, column 7
Any ideas what I'm doing wrong? Thanks!
A few issues in no particular order.
First, in the body of a row-level trigger, you need to use :new and :old to reference the new and old records. The leading colon is necessary. So your WHERE clause would need to be
WHERE PROJECTID = :new.PROJECTID
Second, if you are running your CREATE TRIGGER in SQL*Plus, you can get a list of the errors and warnings using the SHOW ERRORS command, i.e.
SQL> show errors
You could also query the DBA_ERRORS table (or ALL_ERRORS or USER_ERRORS depending on your privilege level) but that's not something you normally need to resort to.
Third, assuming the syntax errors get corrected, you're going to get a mutating table error if you use this logic. A row level trigger on table A (TPM_TRAININGPLAN in this case) cannot query table A because the table may be in an inconsistent state. You can work around that, as Tim shows in his article, by creating a package with a collection, initializing that collection in a before statement trigger, populating the data in the collection in a row-level trigger, and then processing the modified rows in an after statement trigger. That's a decent amount of complexity to add to the system, however, since you'll have to manage multiple different objects.
Generally, you'd be better off implementing this logic as part of whatever API you use to manipulate the TPM_TRAININGPLAN table. If that is a stored procedure, it makes much more sense to put the logic to update TPM_PROJECT in that stored procedure rather than putting it in a trigger. It is notoriously painful to try to debug an application that has a lot of logic embedded in triggers because that makes it very difficult for developers to follow exactly what operations are being performed. Alternately, you could remove the TRAININGDELIVERYSTART column from TPM_PROJECT table and just compute the minimum start date at runtime.
Fourth, if your trigger fires on inserts, updates, and deletes, you can't simply reference :new values. :new is valid for inserts and updates but it is going to be NULL if you're doing a delete. :old is valid for deletes and updates but is going to be NULL if you're doing an insert. That means that you probably need to have logic along the lines of (referencing Tim's package solution)
BEGIN
IF inserting
THEN
trigger_api.tab1_row_change(p_id => :new.projectid, p_action => 'INSERT');
ELSIF updating
THEN
trigger_api.tab1_row_change(p_id => :new.projectid, p_action => 'UPDATE');
ELSIF deleting
THEN
trigger_api.tab1_row_change(p_id => :old.projectid, p_action => 'DELETE');
END IF;
END;
As Justin Cave have suggested, you can calculate the minimum start date when you need it. It might help if you create an index on (projectid, startdate);
If you really have a lot of projects and training plans, another solution could be to create a MATERIALIZED VIEW that has all the data that you need:
CREATE MATERIALIZED VIEW my_view
... add refresh options here ...
AS
SELECT t.projectid, MIN(t.start_date) AS min_start_date
FROM TPM_TRAININGPLAN t
GROUP BY t.projectid;
(sorry, don't have Oracle running, the above code is just for the reference)

Can i use NOT NULL constraint with a condition? in PostgreSQL

im a newbie to PostgreSQL, is there any way that i can make some tuples not deletable if some condition holds? to be specific, suppose i have:
Table Males( Name_A, Profession)
Table Students( Names_B, Date_birth)
where Names_B references Names_A, how can i make sure that only those Names_A are "not deletable" whose Date_birth="xx/yy/zz"
sorry if i couldnt clearly explain it, havnt found anything in DDL using NOT NULL constraint to write this up.
Thanks in advance for the help!
CREATE FUNCTION protect_delete() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF OLD.date_birth = 'xx/yy/zz' THEN -- don't actually use this date format
RETURN NULL; -- don't delete
ELSE
RETURN OLD;
END IF;
END;
$$;
CREATE TRIGGER protect_delete BEFORE DELETE ON students FOR EACH ROW
EXECUTE PROCEDURE protect_delete();
You can use a PostgreSQL rule:
create rule rule_test as
on delete to test
-- old is a reference to your table
where old.birth = '2011-1-1' -- or whatever condition you want
do instead nothing;
One a big table this may run faster since this will modify the query itself and rewrite the query with the condition instead of checking each row. (Triggers may be more powerful and easier to understand if you are planning to do a lot of this type of stuff.)
create rule - http://www.postgresql.org/docs/current/static/sql-createrule.html
rules vs. triggers - http://www.postgresql.org/docs/current/static/rules-triggers.html
See Postgres trigger documentation for information on creating triggers. It sounds like you want a row level trigger.
"A row-level trigger fired before an operation ... can return NULL to skip the operation for the current row. This instructs the executor to not perform the row-level operation that invoked the trigger (the insertion or modification of a particular table row). "
So within the trigger test for your condition and return null to prevent the deletion, return the trigger row to allow the deletion to continue.