SQL Trigger time check + - 1hour [duplicate] - sql

I am writing a simple trigger that is supposed to just send a message with the updated Count of rows as well as the old value of Gender and the updated value of Gender. When i run an update however I am getting the error that the table is mutating and the table might not be able to see it but I'm not exactly sure why.
trigger
create or replace trigger updatePERSONS
after update
on PERSONS
for each row
declare
n int;
oldGender varchar(20):= :OLD.Gender;
newGender varchar(20):= :NEW.Gender;
begin
select Count(*)
into n
from PERSONS;
if (oldGender != newGender) then
dbms_output.put_line('There are now '|| n || ' rows after update. Old gender: ' || oldGender
|| ', new Gender: ' || newGender);
end if;
End;
`
i know it has to do with the select statement after begin but how else would i get count of rows?

As #San points out, a row-level trigger on persons cannot generally query the persons table.
You'd need two triggers, a row-level trigger that can see the old and new gender and a statement-level trigger that can do the count. You could also, if you're using 11g, create a compound trigger with both row- and statement-level blocks.
create or replace trigger trg_stmt
after update
on persons
declare
l_cnt integer;
begin
select count(*)
into l_cnt
from persons;
dbms_output.put_line( 'There are now ' || l_cnt || ' rows.' );
end;
create or replace trigger trg_row
after update
on persons
for each row
begin
if( :new.gender != :old.gender )
then
dbms_output.put_line( 'Old gender = ' || :old.gender || ', new gender = ' || :new.gender );
end if;
end;

Related

Creating a table if it doesn't exist already

I'm trying to create a table if it doesn't exist already. I'm currently checking to see if it exists in DBA_TABLES first and if that query returns nothing then insert. Is there a way to just check in the same statement so I don't have to break it up into separate queries?
This is what I have currently.
BEGIN
SELECT COUNT(*)
INTO lvnTableExists
FROM DBA_TABLES
WHERE Table_Name = 'SOME_TABLE';
IF lvnTableExists = 0 THEN
EXECUTE IMMEDIATE 'CREATE TABLE SOME_TABLE AS (SELECT * FR0M OTHER_TABLE)';
END IF;
END;
This is something that I'm going for.
DECLARE
sql VARCHAR2(100000);
BEGIN
sql := 'CREATE TABLE SOME_TABLE ' ||
'AS (SELECT * FROM OTHER_TABLE) ' ||
'WHERE NOT EXISTS (SELECT NULL ' ||
'FROM DBA_OBJECTS d WHERE d.Object_Name = 'SOME_TABLE' AND ' ||
'd.Owner = 'SOME_TABLE')';
EXECUTE IMMEDIATE sql;
END;
The problem is that, you can't put a WHERE NOT EXISTS in a CREATE TABLE AS statement.
Yes, that's really a shame that Oracle doesn't have that functionality. I'm sure it will come some day. Until then, if you want to write a PL/SQL wrapper, why not do it like that:
DEClARE
table_exists_already exception;
pragma exception_init(table_exists_already, -955 );
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE SOMETABLE...';
EXCEPTION WHEN table_exists_already THEN
DBMS_OUTPUT.PUT_LINE('Table sometable already exists');
END;
/

Create trigger using cursor

I am having difficulty figuring out how to implement a before insert trigger using a cursor that will compare the insert number with the max value of the 1 column table (NUM), then print out the corresponding message. Any help would be greatly appreciated.
create or replace trigger MAXSOME
before insert on SOMENUMBERS for each row
declare
cursor pointer is
select max(NUM) from SOMENUMBERS;
x number;
begin
x := :new.NUM;
if x > pointer.num then
dbms_output.put_line('The new number ' || x || ' is greater than the greatest number in the table.');
elsif x := pointer then
dbms_output.put_line('The new number ' || x || ' is the same as the greatest number in the table.');
else
dbms_output.put_line(pointer.num || ' is still the largest number in the table.');
end if;
end;
/
If you declare a cursor you have to open it in a loop but you don't even need a cursor. Using dbms_output is not going to work very well as one session may insert and you may be waiting for the output using another session.. Create another table and log the output there.
I see Knuckles suggests an autonomous transaction but my test case succeeded without one.
create or replace trigger MAXSOME
before insert on SOMENUMBERS
for each row
declare
x SOMENUMBERS.NUM%TYPE;
begin
select max(NUM) into x from SOMENUMBERS;
if x > :new.NUM then
dbms_output.put_line('The new number ' || :new.NUM || ' is greater than the greatest number in the table.');
elsif x = :new.NUM then
dbms_output.put_line('The new number ' || :new.NUM || ' is the same as the greatest number in the table.');
else
dbms_output.put_line(:new.NUM || ' is still the largest number in the table.');
end if;
end;

Trigger error with after insert

![enter image description here][1]I am creating a trigger that whenever I insert info into my table, I count the total row numbers and print the new added row. Here is my code:
Create or replace trigger TR_everyInsert
After INSERT On PERSONS
For each row
Declare
rowNumber int;
PERSON_NAME varchar(30);
gender varchar(30);
color varchar(30);
Begin
select PERSON_NAME,GENDER,COLOR
From PERSONS
Where PERSON_NAME=:new.PERSON_NAME;
select count(*) as rowNumber
from PERSONS;
if inserting then
DBMS_OUTPUT.PUT_LINE ('There are ' || To_CHAR(rowNumber));
DBMS_OUTPUT.PUT_LINE ('New added info is ' || PERSON_NAME || 'with gender ' ||
GENDER || 'with color ' || color);
end if;
end;
/
However, I got compile error saying "into clause expected", what is the problem please?
First, you can't have a PL/SQL block that just executes a SELECT. You need to do something with the data. If you expect the query to return exactly 1 row, do a SELECT INTO. If you expect the query to return more than 1 row, you'd want to open a cursor that you'd iterate over.
Second, in a row-level trigger, you cannot generally query the table itself. You'll generally end up with a mutating table exception (there are some special cases where you can do this but that severely limits your flexibility going forward so it's something that should be avoided). To get row-level information, just use the various columns from your :new pseudo-record. To get the count, you'd realistically want to use a statement-level trigger. Depending on the Oracle version, you could create a compound trigger that has row- and statement-level components as well.
Third, t doesn't really make sense to have an IF inserting statement in a trigger that is only defined on the insert operation. It would only make sense to have that sort of statement if your trigger was defined on multiple operations (say INSERT OR UPDATE) and you wanted to do something different depending on which operation caused the trigger to fire.
Finally, you'll want your local variables to be named something that is distinct from the names of any columns. Most people adopt some sort of naming convention to disambiguate local variables, package global variables, and parameters from column names. I prefer prefixes l_, g_, and p_ for local variables, package global variables, and parameters which is a reasonably common convention in the Oracle community. You may prefer something else.
Something like
-- A row-level trigger prints out the data that is being inserted
Create or replace trigger TR_everyInsert_row
After INSERT On PERSONS
For each row
Begin
DBMS_OUTPUT.PUT_LINE ('New added info is ' || :new.PERSON_NAME ||
' with gender ' || :new.GENDER ||
' with color ' || :new.color);
end;
-- A statement-level trigger prints out the current row count
Create or replace trigger TR_everyInsert_stmt
After INSERT On PERSONS
Declare
l_cnt integer;
Begin
select count(*)
into l_cnt
from persons;
DBMS_OUTPUT.PUT_LINE ('There are ' || To_CHAR(l_cnt) || ' rows.');
end;
The error message is pretty clear. You need to place the result of both your queries INTO the variables you declared:
Create or replace trigger TR_everyInsert
After INSERT On PERSONS
For each row
Declare
lv_rowNumber int;
lv-_PERSON_NAME varchar(30);
lv_gender varchar(30);
lv_color varchar(30);
Begin
select PERSON_NAME,GENDER,COLOR
into lv_person_name, lv_gender, lv_color
From PERSONS
Where PERSON_NAME=:new.PERSON_NAME;
select count(*) into lv_rowNumber
from PERSONS;
if inserting then
DBMS_OUTPUT.PUT_LINE ('There are ' || To_CHAR(rowNumber));
DBMS_OUTPUT.PUT_LINE ('New added info is ' || PERSON_NAME || 'with gender ' ||
GENDER || 'with color ' || color);
end if;
end;
/
I would advice you to give your variables different names than your columns. It could make the code confusing to read...

Trigger that restricts by date

Im trying to create a trigger that restricts the amount a reader can read in a given month.
CREATE OR REPLACE trigger Readings_Limit
Before update or insert on reading
for each row declare
readingcount integer;
max_read integer := 5;
Begin
Select count(*) into readingcount
from (select *
from Reading
where to_char(DateRead, 'YYYY-MM') = to_char(DateRead, 'YYYY-MM'))
where employeeid = :new.employeeid;
if :old.employeeid = :new.employeeid then
return;
else
if readingcount >= max_read then
raise_application_error (-20000, 'An Employee can only read 5 per month');
end if;
end if;
end;
This restricts the reader to 5 max in total no matter the month, i can't seem to get it to be 5 max each month. any ideas greatly appreciated!
Try to rewrite your trigger like this:
CREATE OR REPLACE trigger Readings_Limit
Before update or insert on reading
for each row
declare
readingcount integer;
max_read integer := 5;
Begin
Select count(*) into readingcount
from Reading
where DateRead between trunc(sysdate,'MM') and last_day(sysdate)
and employeeid = :new.employeeid;
if :old.employeeid = :new.employeeid then
return;
else
if readingcount >= max_read then
raise_application_error (-20000, 'An Employee can only read 5 per month');
end if;
end if;
end;
You add actual month into yout select and you avoid unnecessary date conversion.
I don't understand the condition
if :old.employeeid = :new.employeeid then
Does it mean, the trigger should not fire on updates? In this case it is better to make trigger only for insert or use clause if inserting then...
In order to properly create this validation using a trigger a procedure should be created to obtain user-specified locks so the validation can be correctly serialized in a multi-user environment.
PROCEDURE request_lock
(p_lockname IN VARCHAR2
,p_lockmode IN INTEGER DEFAULT dbms_lock.x_mode
,p_timeout IN INTEGER DEFAULT 60
,p_release_on_commit IN BOOLEAN DEFAULT TRUE
,p_expiration_secs IN INTEGER DEFAULT 600)
IS
-- dbms_lock.allocate_unique issues implicit commit, so place in its own
-- transaction so it does not affect the caller
PRAGMA AUTONOMOUS_TRANSACTION;
l_lockhandle VARCHAR2(128);
l_return NUMBER;
BEGIN
dbms_lock.allocate_unique
(lockname => p_lockname
,lockhandle => p_lockhandle
,expiration_secs => p_expiration_secs);
l_return := dbms_lock.request
(lockhandle => l_lockhandle
,lockmode => p_lockmode
,timeout => p_timeout
,release_on_commit => p_release_on_commit);
IF (l_return not in (0,4)) THEN
raise_application_error(-20001, 'dbms_lock.request Return Value ' || l_return);
END IF;
-- Must COMMIT an autonomous transaction
COMMIT;
END request_lock;
To have the least impact on scalability the serialization should be done at the finest level, which for this constraint is per employeeid and month. Types may be used in order to create variables to store this information for each row before the constraint is checked after the statement has completed. These types can be either defined in the database or (from Oracle 12c) in package specifications.
CREATE OR REPLACE TYPE reading_rec
AS OBJECT
(employeeid NUMBER(10) -- Note should match the datatype of reading.employeeid
,dateread DATE);
CREATE OR REPLACE TYPE readings_tbl
AS TABLE OF reading_rec;
The procedure and types can then be used in a compound trigger (assuming at least Oracle 11, this will need to be split into individual triggers in earlier versions)
CREATE OR REPLACE TRIGGER too_many_readings
FOR INSERT OR UPDATE ON reading
COMPOUND TRIGGER
-- Table to hold identifiers of inserted/updated readings
g_readings readings_tbl;
BEFORE STATEMENT
IS
BEGIN
-- Reset the internal readings table
g_readings := readings_tbl();
END BEFORE STATEMENT;
AFTER EACH ROW
IS
BEGIN
-- Store the inserted/updated readings
IF ( INSERTING
OR :new.employeeid <> :old.employeeid
OR :new.dateread <> :old.dateread)
THEN
g_readings.EXTEND;
g_readings(g_readings.LAST) := reading_rec(:new.employeeid, :new.dateread);
END IF;
END AFTER EACH ROW;
AFTER STATEMENT
IS
CURSOR csr_readings
IS
SELECT DISTINCT
employeeid
,trunc(dateread,'MM') monthread
FROM TABLE(g_readings)
ORDER BY employeeid
,trunc(dateread,'MM');
CURSOR csr_constraint_violations
(p_employeeid reading.employeeid%TYPE
,p_monthread reading.dateread%TYPE)
IS
SELECT count(*) readings
FROM reading rdg
WHERE rdg.employeeid = p_employeeid
AND trunc(rdg.dateread, 'MM') = p_monthread
HAVING count(*) > 5;
r_constraint_violation csr_constraint_violations%ROWTYPE;
BEGIN
-- Check if for any inserted/updated readings there exists more than
-- 5 readings for the same employee in the same month. Serialise the
-- constraint for each employeeid so concurrent transactions do not
-- affect each other
FOR r_reading IN csr_readings LOOP
request_lock('TOO_MANY_READINGS_'
|| r_reading.employeeid
|| '_' || to_char(r_reading.monthread, 'YYYYMM'));
OPEN csr_constraint_violations(r_reading.employeeid, r_reading.monthread);
FETCH csr_constraint_violations INTO r_constraint_violation;
IF csr_constraint_violations%FOUND THEN
CLOSE csr_constraint_violations;
raise_application_error(-20001, 'Employee ' || r_reading.employeeid
|| ' now has ' || r_constraint_violation.readings
|| ' in ' || to_char(r_reading.monthread, 'FMMonth YYYY'));
ELSE
CLOSE csr_constraint_violations;
END IF;
END LOOP;
END AFTER STATEMENT;
END;
You need to set the month you are looking at, so if you are considering the current month, make the inner query read like this:
( select * from Reading
where to_char(DateRead,'YYYY-MM') = to_char(DateRead,'YYYY-MM')
and to_char(sysdate,'YYYY-MM') = to_char(DateRead,'YYYY-MM'))
that way it will always compare to the current month and should move as your date moves.

Trigger on every update or insert

I want to create trigger that fires every time any column is changed - whether it is freshly updated or new insert. I created something like this:
CREATE TRIGGER textsearch
BEFORE INSERT OR UPDATE
ON table
FOR EACH ROW
EXECUTE PROCEDURE trigger();
and body of trigger() function is:
BEGIN
NEW.ts := (
SELECT COALESCE(a::text,'') || ' ' ||
COALESCE(b::int,'') || ' ' ||
COALESCE(c::text,'') || ' ' ||
COALESCE(d::int, '') || ' ' ||
COALESCE(e::text,'')
FROM table
WHERE table.id = new.id);
RETURN NEW;
END
I hope it is clear what I want to do.
My problem is that trigger fires only on update, not on insert. I guess that this isn't working because I have BEFORE INSERT OR UPDATE, but if I change it to AFTER INSERT OR UPDATE then it doesn't work neither for INSERT nor UPDATE.
you need to use the NEW record directly:
BEGIN
NEW.ts := concat_ws(' ', NEW.a::text, NEW.b::TEXT, NEW.c::TEXT);
RETURN NEW;
END;
The advantage of concat_ws over || is that concat_ws will treat NULL values differently. The result of 'foo'||NULL will yield NULL which is most probably not what you want. concat_ws will use an empty string NULL values.
It doesn't work because you're calling SELECT inside the function.
When it runs BEFORE INSERT then there isn't a row to select, is there?
Actually, BEFORE UPDATE you'll see the "old" version of the row anyway, won't it?
Just directly use the fields: NEW.a etc rather than selecting.
As an edit - here is an example showing what the trigger function can see. It's exaclty as you'd expect in a BEFORE trigger.
BEGIN;
CREATE TABLE tt (i int, t text, PRIMARY KEY (i));
CREATE FUNCTION trigfn() RETURNS TRIGGER AS $$
DECLARE
sv text;
BEGIN
SELECT t INTO sv FROM tt WHERE i = NEW.i;
RAISE NOTICE 'new value = %, selected value = %', NEW.t, sv;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigtest BEFORE INSERT OR UPDATE ON tt FOR EACH ROW EXECUTE PROCEDURE trigfn();
INSERT INTO tt VALUES (1,'a1');
UPDATE tt SET t = 'a2' WHERE i = 1;
ROLLBACK;
in your COALESCE statements when you cast b::int i had to change your coalesce to use integer place holder instead. As mentioned by a_horse_with_no_name this can end up with null values but you can see how to make your specific code example run. I included the "RAISE NOTICE" lines for debug purposes only.
Based on your provided information the following works for me:
CREATE TABLE my_table (id SERIAL NOT NULL,a TEXT,b INTEGER,c TEXT,d INTEGER,e TEXT);
CREATE OR REPLACE FUNCTION my_triggered_procedure() RETURNS trigger AS $$
BEGIN
if(TG_OP = 'UPDATE' OR TG_OP = 'INSERT') THEN
NEW.ts := (SELECT COALESCE(a::text,'') || ' ' ||
COALESCE(b::int,0) || ' ' ||
COALESCE(c::text,'') || ' ' ||
COALESCE(d::int, 0) || ' ' ||
COALESCE(e::text,'')
FROM my_table
WHERE id=NEW.id);
RAISE NOTICE 'INSERT OR UPDATE with new ts = %',NEW.ts;
RETURN NEW;
ELSIF (TG_OP = 'DELETE') THEN
OLD.ts := ' ';
RAISE NOTICE 'DELETED old id: %',OLD.id;
RETURN OLD;
END IF;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER text_search
AFTER INSERT OR UPDATE OR DELETE
ON my_table
FOR EACH ROW
EXECUTE PROCEDURE my_triggered_procedure();
INSERT INTO my_table (a,b,c,d,e) VALUES('text11',12,'text21',3,'text4');
>NOTICE: INSERT OR UPDATE with new ts = text11 12 text21 3 text4
>INSERT 0 1
DELETE FROM my_table WHERE id=24;
>NOTICE: DELETED ID = 24
>DELETE 1
PostgreSQL::Trigger Procedures