I am trying to create some kind of trigger to prevent a row being edited if it is after today's date (will use SYSDATE to get that).
I am unsure about how to do this as I am new to PL/SQL and would think perhaps some kind of package that gets the date using a cursor then uses a function to return a boolean to a procedure which then somehow stops the DML statement from firing?
Thanks in advance
Obviously you need a date column as your target. Truncating SYSDATE will give you midnight. Consequently if a truncated SYSDATE is greater than another date it must be at least the next day.
Raising an application error will cause the update to fail. Note that if you're updating multiple rows a single failure will rollback all the changes.
create or replace trigger your_trg
before update on your_table
for each row
begin
if trunc(sysdate) > :old.whatever_date then
raise_application_error(-20000, 'It is too late to change this record');
end if;
end;
the solution you are looking for is VPD Column masking.
mainly used for security purposes, VPD enables you to define row/colums level rules for data access and display.
Related
my trigger doesn't work, when I try to update a query. Any ideas why?
I feel like it is related to the WHERE conditions when I try to set variables.
I tried to do it without the WHERE conditions, but it still didn't work.
Any ideas why? Thank You!
CREATE OR REPLACE TRIGGER SYSTEM.Product_Price_Check
BEFORE UPDATE ON SYSTEM.product FOR EACH ROW
DECLARE
min_price NUMBER(19,4);
new_price NUMBER(19,4);
BEGIN
SELECT (StandardCost*1.2)
INTO min_price
FROM SYSTEM.product
WHERE ProductID = :new.ProductID;
SELECT ListPrice
INTO new_price
FROM SYSTEM.product
WHERE ProductID = :new.ProductID;
IF new_price < min_price THEN
ROLLBACK;
--DBMS_OUTPUT.PUT_LINE('the price can’t be below '||CAST(min_price as VARCHAR(25)));
--RAISE VALUE_ERROR;
--ELSE
--DBMS_OUTPUT.PUT_LINE('Price was successfully changed');
END IF;
END;
I suppose this is a homework assignment and you have been told to use a trigger. Long years on this site have taught me that teachers love setting assignments which demand the misuse of triggers.
In real life the only correct way to enforce such a rule is with a check constraint:
alter table product add constraint price_check
check (standard_cost * 1.2 >= min_price)
Show your error.
Probably your trigger is mutating
The session that issued the triggering statement cannot query or modify a mutating table. This restriction prevents a trigger from seeing an inconsistent set of data.
As others have pointed out, you should not be creating objects such as tables or triggers in the SYSTEM schema. Create yourself a user, grant it the necessary privileges (good practice), and use that user and its schema for development purposes.
Next - in a row triggers (one with FOR EACH ROW in it) you cannot access the table upon which the trigger is defined, which in this case is the PRODUCT table. Fortunately, you don't really need to. The values you want are already in the :OLD or :NEW pseudo-rows - I'm guessing here that you really want to use the :NEW values:
CREATE OR REPLACE TRIGGER PRODUCT_PRICE_CHECK
BEFORE UPDATE ON PRODUCT
FOR EACH ROW
DECLARE
nMin_price NUMBER := :NEW.STANDARD_COST * 1.2;
BEGIN
IF :NEW.LISTPRICE < nMin_price THEN
DBMS_OUTPUT.PUT_LINE('List price can’t be below '|| nMin_price);
RAISE VALUE_ERROR;
END PRICE_CHECK;
Also, you can't execute a ROLLBACK or COMMIT in a trigger - Oracle doesn't allow this to happen.
I want that every time a data is input in oracle table the date and time must automatically be updated in one of the column named 'CREATION_DATE'.
Setting default value of SYSDATE is more efficient than a trigger. As helpc mentioned, a default value can be overridden if NULL is explicitly provided in the INSERT. If you don't intend to pass date time thru application at all, you can define the column as NOT NULL with a default as sysdate .
A trigger will do what you want. I think something like this is what you are looking for:
CREATE OR REPLACE TRIGGER date_trigger
AFTER INSERT ON your_table
FOR EACH ROW
WHEN (new.your_table> 0)
BEGIN
:NEW.CREATION_DATE:= SYSDATE;
END;
/
Depending on your needs I usually like to add both a create_date and an update_date column to pick up timestamps for changes that may occur later.
This might appear a simple query for most of you, but I am a beginner in Oracle DB.
A table has been created with below script-
CREATE TABLE PLAN_TABLE
(
PL_ID DECIMAL(10,0) PRIMARY KEY NOT NULL
,PL_NAME VARCHAR2(300) DEFAULT NULL
,UPDATED_TS TIMESTAMP DEFAULT SYSDATE NOT NULL
,DELETE_FLAG DECIMAL(10,0) DEFAULT 0 NOT NULL
);
The requirement is to have SYSDATE for UPDATED_TS for any new record inserted into the table and also in case when the DELETE_FLAG is updated to 1. Can it be done by trigger?
The below trigger was created-
CREATE OR REPLACE TRIGGER PT_BEFORE_INSERT_TR
BEFORE INSERT ON PLAN_TABLE
FOR EACH ROW
BEGIN
SELECT SYSDATE INTO :new.UPDATED_TS FROM DUAL;
dbms_output.put_line('Inserted');
END;
/
Below error was encountered while inserting record into the table-
error: ORA-04091: table I60_SCH04.PLAN_TABLE is mutating, trigger/function may not see it
Can you please help in letting me know that where am I committing the mistake? Is there any better way to achieve the requirement based upon INSERT/UPDATE?
The actual error you get is due to the fact that you try to select from a table that you actually are changing. To prevent the issue there are a couple of methods, but in you case things are really simple.
SYSDATE is a function, that you could call directly inside PL/SQL block (which a trigger actually is) and use the value returned to update the set the column value
CREATE OR REPLACE TRIGGER PT_BEFORE_INSERT_TR
BEFORE INSERT ON PLAN_TABLE
FOR EACH ROW
BEGIN
:new.UPDATED_TS := sysdate;
dbms_output.put_line('Inserted');
END;
/
OK, this covers the insert part.
For updating - once again, many options. One could be - change your trigger to BEFORE INSERT OR UPDATE ON PLAN_TABLE.
In this case whenever you issue update or insert - this trigger is fired for each row and updates the date column accordingly.
And of course you could use particular checks available in triggers, something like
IF INSERTING OR UPDATING('DELETE_FLAG') THEN
...
END IF;
and code in the logic you need.
I'm writing an application and I would like to know whether is it possible to get this data permission configuration on the Database side (Oracle) not the application side.
It's basically related to some tables that have to store historical data that can not be edited the day after they were typed...
Table : X
Fields :
WhatEver as varchar2
MyDate as DateTime
For each row, if (Current Date = MyDate), editing is allowed. Otherwise, No.
Is that possible please ? Am I required to use Oracle Label Security ?
I'm looking for something that can be managed as Access Rights (Grant/Revoke...) The usage would be that the administrator can Create, Grant or Revoke such a permission to a user or a role.
Thanks.
A row level trigger should do the trick.
create or replace trigger my_trigger
before update on my_table
for each row
begin
-- Only compare the date part (trunc removes the time)
if trunc(:old.mydate)!=trunc(sysdate)
then raise_application_error(-20000,'Updates only allowed on the day of entry');
end if;
end;
Of course, entries added just before midnight have a very short edit time.
Also, you should somehow disable the possibility to update the mydate field. Some extra lines in the trigger will take care of that.
I recently started working on a large complex application, and I've just been assigned a bug due to this error:
ORA-04091: table SCMA.TBL1 is mutating, trigger/function may not see it
ORA-06512: at "SCMA.TRG_T1_TBL1_COL1", line 4
ORA-04088: error during execution of trigger 'SCMA.TRG_T1_TBL1_COL1'
The trigger in question looks like
create or replace TRIGGER TRG_T1_TBL1_COL1
BEFORE INSERT OR UPDATE OF t1_appnt_evnt_id ON TBL1
FOR EACH ROW
WHEN (NEW.t1_prnt_t1_pk is not null)
DECLARE
v_reassign_count number(20);
BEGIN
select count(t1_pk) INTO v_reassign_count from TBL1
where t1_appnt_evnt_id=:new.t1_appnt_evnt_id and t1_prnt_t1_pk is not null;
IF (v_reassign_count > 0) THEN
RAISE_APPLICATION_ERROR(-20013, 'Multiple reassignments not allowed');
END IF;
END;
The table has a primary key "t1_pk", an "appointment event id"
t1_appnt_evnt_id and another column "t1_prnt_t1_pk" which may or may
not contain another row's t1_pk.
It appears the trigger is trying to make sure that nobody else with the
same t1_appnt_evnt_id has referred to the same one this row is referring to a referral to another row, if this one is referring to another row.
The comment on the bug report from the DBA says "remove the trigger, and perform the check in the code", but unfortunately they have a proprietary code generation framework layered on top of Hibernate, so I can't even figure out where it actually gets written out, so I'm hoping that there is a way to make this trigger work. Is there?
I think I disagree with your description of what the trigger is trying to
do. It looks to me like it is meant to enforce this business rule: For a
given value of t1_appnt_event, only one row can have a non-NULL value of
t1_prnt_t1_pk at a time. (It doesn't matter if they have the same value in the second column or not.)
Interestingly, it is defined for UPDATE OF t1_appnt_event but not for the other column, so I think someone could break the rule by updating the second column, unless there is a separate trigger for that column.
There might be a way you could create a function-based index that enforces this rule so you can get rid of the trigger entirely. I came up with one way but it requires some assumptions:
The table has a numeric primary key
The primary key and the t1_prnt_t1_pk are both always positive numbers
If these assumptions are true, you could create a function like this:
dev> create or replace function f( a number, b number ) return number deterministic as
2 begin
3 if a is null then return 0-b; else return a; end if;
4 end;
and an index like this:
CREATE UNIQUE INDEX my_index ON my_table
( t1_appnt_event, f( t1_prnt_t1_pk, primary_key_column) );
So rows where the PMNT column is NULL would appear in the index with the inverse of the primary key as the second value, so they would never conflict with each other. Rows where it is not NULL would use the actual (positive) value of the column. The only way you could get a constraint violation would be if two rows had the same non-NULL values in both columns.
This is perhaps overly "clever", but it might help you get around your problem.
Update from Paul Tomblin: I went with the update to the original idea that igor put in the comments:
CREATE UNIQUE INDEX cappec_ccip_uniq_idx
ON tbl1 (t1_appnt_event,
CASE WHEN t1_prnt_t1_pk IS NOT NULL THEN 1 ELSE t1_pk END);
I agree with Dave that the desired result probalby can and should be achieved using built-in constraints such as unique indexes (or unique constraints).
If you really need to get around the mutating table error, the usual way to do it is to create a package which contains a package-scoped variable that is a table of something that can be used to identify the changed rows (I think ROWID is possible, otherwise you have to use the PK, I don't use Oracle currently so I can't test it). The FOR EACH ROW trigger then fills in this variable with all rows that are modified by the statement, and then there is an AFTER each statement trigger that reads the rows and validate them.
Something like (syntax is probably wrong, I haven't worked with Oracle for a few years)
CREATE OR REPLACE PACKAGE trigger_pkg;
PROCEDURE before_stmt_trigger;
PROCEDURE for_each_row_trigger(row IN ROWID);
PROCEDURE after_stmt_trigger;
END trigger_pkg;
CREATE OR REPLACE PACKAGE BODY trigger_pkg AS
TYPE rowid_tbl IS TABLE OF(ROWID);
modified_rows rowid_tbl;
PROCEDURE before_stmt_trigger IS
BEGIN
modified_rows := rowid_tbl();
END before_each_stmt_trigger;
PROCEDURE for_each_row_trigger(row IN ROWID) IS
BEGIN
modified_rows(modified_rows.COUNT) = row;
END for_each_row_trigger;
PROCEDURE after_stmt_trigger IS
BEGIN
FOR i IN 1 .. modified_rows.COUNT LOOP
SELECT ... INTO ... FROM the_table WHERE rowid = modified_rows(i);
-- do whatever you want to
END LOOP;
END after_each_stmt_trigger;
END trigger_pkg;
CREATE OR REPLACE TRIGGER before_stmt_trigger BEFORE INSERT OR UPDATE ON mytable AS
BEGIN
trigger_pkg.before_stmt_trigger;
END;
CREATE OR REPLACE TRIGGER after_stmt_trigger AFTER INSERT OR UPDATE ON mytable AS
BEGIN
trigger_pkg.after_stmt_trigger;
END;
CREATE OR REPLACE TRIGGER for_each_row_trigger
BEFORE INSERT OR UPDATE ON mytable
WHEN (new.mycolumn IS NOT NULL) AS
BEGIN
trigger_pkg.for_each_row_trigger(:new.rowid);
END;
With any trigger-based (or application code-based) solution you need to
put in locking to prevent data corruption in a multi-user environment.
Even if your trigger worked, or was re-written to avoid the mutating table
issue, it would not prevent 2 users from simultaneously updating
t1_appnt_evnt_id to the same value on rows where t1_appnt_evnt_id is not
null: assume there are currenly no rows where t1_appnt_evnt_id=123 and
t1_prnt_t1_pk is not null:
Session 1> update tbl1
set t1_appnt_evnt_id=123
where t1_prnt_t1_pk =456;
/* OK, trigger sees count of 0 */
Session 2> update tbl1
set t1_appnt_evnt_id=123
where t1_prnt_t1_pk =789;
/* OK, trigger sees count of 0 because
session 1 hasn't committed yet */
Session 1> commit;
Session 2> commit;
You now have a corrupted database!
The way to avoid this (in trigger or application code) would be to lock
the parent row in the table referenced by t1_appnt_evnt_id=123 before performing the check:
select appe_id
into v_app_id
from parent_table
where appe_id = :new.t1_appnt_evnt_id
for update;
Now session 2's trigger must wait for session 1 to commit or rollback before it performs the check.
It would be much simpler and safer to implement Dave Costa's index!
Finally, I'm glad no one has suggested adding PRAGMA AUTONOMOUS_TRANSACTION to your trigger: this is often suggested on forums and works in as much as the mutating table issue goes away - but it makes the data integrity problem even worse! So just don't...
I had similar error with Hibernate. And flushing session by using
getHibernateTemplate().saveOrUpdate(o);
getHibernateTemplate().flush();
solved this problem for me. (I'm not posting my code block as I was sure that everything was written properly and should work - but it did not until I added the previous flush() statement). Maybe this can help someone.