I am creating a trigger that should compare of the values being inserted with one that already exists in the table. The old referencer doesn't work here because I am inserting, but how can I reference something that already exists?
Here is my tables and trigger:
create table events(eid char(2) primary key, cid char(2));
create table activities(mid char(2), eid char(2),
primary key (mid, eid),
constraint activities_fk foreign key (eid) references events(eid));
create or replace trigger check_valid
before insert or update on activities
for each row when (old.mid=new.mid)
declare
v_eid char(2);
v_cid char(2);
n_cid char(2);
begin
select eid into v_eid from activities
where mid=:new.mid;
select cid into v_cid from events
where eid=v_eid;
select cid into n_cid from events
where eid=:new.eid;
if v_cid=n_cid then
raise_application_error(-20000, 'Error');
end if;
end check_valid;
/
show errors;
You can't generally select from the table you're inserting into in a trigger. This is the mutating table problem, or as I often call it, the "damn mutating table problem".
Basically, don't do this. It's a bad idea. What happens if you have two sessions operating on the table at once? The trigger fires and neither session sees what the other has done until the commit, which is after the trigger. Then you've got unexpected data in your database.
Tom Kyte says, "when I hit a mutating table error, I've got a serious fatal flaw
in my logic."
Related
I have a trigger function:
CREATE OR REPLACE FUNCTION Day_21_bankTriggerFunction()
RETURNS TRIGGER
LANGUAGE plpgsql
AS
$$
DECLARE
act VARCHAR(30);
BEGIN
SELECT account_number INTO act
DELETE FROM depositor
WHERE depositor.account_number = act;
RETURN act;
END;
$$;
\`
and then I have a trigger:
CREATE TRIGGER Day_21_bankTrigger AFTER DELETE on account FOR EACH ROW EXECUTE PROCEDURE Day_21_bankTriggerFunction()
The thought behind this is that is an account was deleted from the account table then this should trigger the function to run and then delete all records on the depositor table where that account is present.
I can create the trigger function and trigger without an issues but if I attempt to delete an account from the account table...I still see the account # in the depositor table when I shouldn't.
Any thoughts?
above is what I tried. Expected results would be to delete an account from the account table and then the trigger function should kick off and remove that account from the depositor table
Usually, relationships of any mode between tables are created by foreign keys. This is the best way and are standards for DBs. Using foreign keys you can control your data. SQL sample:
CREATE TABLE contacts(
contact_id INT GENERATED ALWAYS AS IDENTITY,
customer_id INT,
contact_name VARCHAR(255) NOT NULL,
email VARCHAR(100),
PRIMARY KEY(contact_id),
CONSTRAINT fk_customer
FOREIGN KEY(customer_id)
REFERENCES customers(customer_id)
ON DELETE SET NULL
ON UPDATE SET NULL
);
Third, specify the parent table and parent key columns referenced by the foreign key columns in the REFERENCES clause.
Finally, specify the delete and update actions in the ON DELETE and ON UPDATE clauses.
The delete and update actions determine the behaviors when the primary key in the parent table is deleted and updated. Since the primary key is rarely updated, the ON UPDATE action is not often used in practice. We’ll focus on the ON DELETE action.
PostgreSQL supports the following actions after updating or deleting:
SET NULL (Set values to NULL if data exists on referencing table)
SET DEFAULT (Set values to DEFAULT VALUES of this field if data exists on referencing table)
RESTRICT (Similar to NO ACTION)
NO ACTION (Can not update or delete data if exists on referencing table)
CASCADE (Delete all data if exists on referencing table)
I wrote for you a sample trigger function:
CREATE OR REPLACE FUNCTION Day_21_bankTriggerFunction()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
declare
act varchar(30);
begin
-- get account_number from deleted record
act = old.account_number;
-- SECTION-1 :: Protect deleting if existing data
if (exists(select 1 from depositor where account_number = act)) then
return null;
end if;
-- SECTION-1 :: END
-- SECTION-2 :: Delete all data in the anothers table if exists */
delete from depositor where account_number = act;
return old;
-- SECTION-2 :: END
end
$function$;
CREATE TRIGGER Day_21_bankTrigger
BEFORE DELETE on account
FOR EACH ROW EXECUTE PROCEDURE Day_21_bankTriggerFunction();
Inside my trigger function, I have written two types of SQL codes. (SECTION-1, SECTION-2). You must choose one of them.
I am trying to implement after insert trigger in PLSQL. The goal is to check if there are multiple (>1) rows having specific status for each client. If so I'd like to rise an exception and roll the insertion back.
I am struggling with implementing warning-free query, which causes error during insertion. How could I manage this?
Here is my implemented trigger which I guess needs some changes.
CREATE TRIGGER blatrigger
AFTER INSERT
ON BLATABLE
REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
DECLARE
exception_name EXCEPTION;
PRAGMA EXCEPTION_INIT (exception_name, -20999);
BEGIN
if (select count(*) as counter from BLATABLE where CLIENTID = :NEW.CLIENTID and STATUS='PENDING').counter > 1
THEN
raise exception_name;
END IF;
END;
Here is the table itself:
create table BLATABLE
(
ID NUMBER(19) not null primary key,
CLIENTID NUMBER(10),
CREATED TIMESTAMP(6),
STATUS VARCHAR2(255 char)
);
The goal is to check if there are multiple (>1) rows having specific status for each client. If so I'd like to rise an exception and roll the insertion back.
No need for a trigger. It looks like a simple unique constraint should get the job done here:
create table blatable (
id number(19) not null primary key,
clientid number(10),
created timestamp(6),
status varchar2(255 char),
constraint blaconstraint unique (clientid, status)
);
The unique constraint prevents duplicates on (clientid, status) across the whole table. If a DML operation (insert, update) attempts to generate a duplicate, an error is raised and the operation is rolled back.
If, on the other end, you want to allow only one "PENDING" status per user, then you can use a unique index as follows:
create unique index bla_index
on blatable( (case when status = 'PENDING' then clientid end) );
Use a Statement Level Trigger, rather than a Row Level by removing FOR EACH ROW, and converting to your code as below :
CREATE OR REPLACE TRIGGER blatrigger
AFTER INSERT ON BLATABLE
REFERENCING NEW AS NEW OLD AS OLD
DECLARE
counter INT;
exception_name EXCEPTION;
PRAGMA EXCEPTION_INIT(exception_name, -20999);
BEGIN
SELECT MAX(COUNT(*))
INTO counter
FROM BLATABLE
WHERE STATUS = 'PENDING'
GROUP BY CLIENTID;
IF counter > 1 THEN
RAISE exception_name;
END IF;
END;
/
where
the SELECT statement need to be removed from IF .. THEN conditional
Most probably, the mutating table error would raise for Row Level Trigger case
Demo
When I enter a new record in one table, I need to have some of the information from the first table be automatically added to the second table. I unsuccessfully tried triggers to do this.
My primary table looks like this:
CREATE TABLE demographics (
person_local_id BIGSERIAL UNIQUE PRIMARY KEY,
first_name VARCHAR(50)...[other lines]
);
I set up the child table like this:
CREATE TABLE pedigree (
pedigree_id BIGSERIAL PRIMARY KEY NOT NULL,
person_local_id BIGSERIAL NOT NULL,
person_sex VARCHAR(10),
father VARCHAR(10) DEFAULT 0,
mother VARCHAR(10) DEFAULT 0,
FOREIGN KEY (person_local_id) REFERENCES demographics (person_local_id)
ON DELETE CASCADE ON UPDATE CASCADE
);
My approach was to create a trigger on the demographics primary table such that any time a record was added to it, a corresponding record would be added to the pedigree table consisting of just the person_local_id. I added a foreign key on the pedigree table that referenced the column in the demographics that I need to copy over to the pedigree table in that column.
Then I created a trigger, but it doesn't work. I tried this with and without the word "EXECUTE".
CREATE TRIGGER into_pedigree AFTER INSERT OR UPDATE ON demographics
FOR EACH ROW EXECUTE INSERT INTO pedigree (person_local_id) SELECT (NEW.person_local_id) FROM NEW;
I keep getting syntax errors but I can't identify the error:
ERROR: syntax error at or near "INSERT"
LINE 2: FOR EACH ROW EXECUTE INSERT INTO pedigree (person_local_id) ...
^
I also tried this, adding the name:
CREATE TRIGGER into_pedigree ON identify_relatives_database.demographics
AFTER INSERT
AS
BEGIN
INSERT INTO pedigree (person_local_id) VALUES (INSERTED.person_local_id)
END;
But I get the error message:
ERROR: syntax error at or near "ON"
LINE 1: CREATE TRIGGER into_pedigree ON demographics
^
I appreciate your assistance.
You may try this.
CREATE TRIGGER [dbo].[Customer_INSERT]
ON [dbo].[demographics]
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #CustomerId INT
SELECT #CustomerId = INSERTED.person_local_id FROM INSERTED
IF EXISTS ( SELECT * FROM PEDIGREE WHERE person_local_id = #CustomerId)
BEGIN
--- here col is the required column name need to be modified,
--- since you are inserting person_local_id from base table which is auto generated and not suppose to be change in any condition
UPDATE PEDIGREE SET COL = INSERTED.COL WHERE person_local_id = #CustomerId
END
ELSE
BEGIN
INSERT INTO pedigree (person_local_id)
VALUES(#CustomerId)
END
END
Although I don't find anything related to your update part. Since you are inserting primary key from base table as foreign key in child table, so for normalization it is not going to changed in any condition. So i don't think you need update part in your trigger Hence your required trigger will be:
CREATE TRIGGER [dbo].[Customer_INSERT]
ON [dbo].[demographics]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO pedigree (person_local_id)
SELECT INSERTED.person_local_id FROM INSERTED
END
You should try this :
CREATE TRIGGER into_pedigree ON demographics
FOR INSERT
AS
insert into pedigree (person_local_id)
values(inserted.person_local_id);
PRINT 'AFTER INSERT trigger fired.'
GO
Let's say we have two tables, Incidents and IncidentTracking:
Incidents (Id INT PRIMARY KEY,
CreatedOn Datetime,
State VARCHAR(50))
IncidentTracking (Id INT PRIMARY KEY,
IncidentId INT FOREIGN KEY REFERENCES TO Incidents.Id,
TrackingDate Datetime,
NewState VARCHAR(50))
How do I insert a new record into IncidentTracking while updating some incidents?
For example, I want to change the State for all incidents that are more than 90 days old to "Outdated", and insert a tracking info record where IncidentId is the updated incident (SCOPE_IDENTITY() maybe?), TrackingDate is GETDATE() and NewState is also "Outdated".
Can it be done all in one statement or should I write a cursor?
I'd use OUTPUT clause.
As IncidentTracking has a foreign key, it is not possible to OUTPUT directly to it. You'll get an error message if you try:
The target table 'dbo.IncidentTracking' of the OUTPUT INTO clause
cannot be on either side of a (primary key, foreign key) relationship.
Found reference constraint 'FK_IncidentTracking_Incidents'.
So, we can use a temporary table or table variable.
Like this:
BEGIN TRANSACTION;
DECLARE #T TABLE (IncidentId int, TrackingDate datetime, NewState varchar(50));
UPDATE [dbo].[Incidents]
SET [State] = 'Outdated'
OUTPUT
inserted.Id AS [IncidentId],
GETDATE() AS [TrackingDate],
inserted.[State] AS [NewState]
INTO #T ([IncidentId], [TrackingDate], [NewState])
WHERE [CreatedOn] < DATEADD(day, -90, GETDATE())
;
INSERT INTO [dbo].[IncidentTracking] ([IncidentId], [TrackingDate], [NewState])
SELECT [IncidentId], [TrackingDate], [NewState]
FROM #T;
COMMIT TRANSACTION;
Write a stored procedure to perform your task. You can put that kind of logic in a stored procedure easily enough.
If you will allow access to the table(s) outside of the procedure and still want the same behavior, a trigger is likely what you want (not a fan of them myself). Make sure, when writing your trigger, you remember that it will run against a recordset not a single record.
You could have a trigger on the state field that inserts into IncidentTracking, then you just need the one update. Or the other way around (adding to IncidentTracking updates State)
But why even have the State in the Incidents table? Just have an IncidentState table where you add every state change, and the latest added one is the current state for the incident.
You'd also probably want to make the State field a StateId column instead of a text code. Connected to a State table containing all the states.
I am trying to update columns in my child table service after delete on parent table cars using function choose_ideal_car which uses some selects over cars and service tables.
Here is part of my sql script:
CREATE TABLE cars(car_id INT CONSTRAINT pk_id_cars);
CREATE TABLE service(
service_id INT CONSTRAINT pk_id_service PRIMARY KEY,
car_id INT CONSTRAINT fk_id_car REFERENCES cars(car_id) ON DELETE SET NULL,
period VARCHAR2(5) CONSTRAINT check_period CHECK period IN ('even','odd','every'),
period VARCHAR2(3) CONSTRAINT check_day CHECK day IN ('mon','tue','wed','thu','fri')
);
CREATE OR REPLACE TRIGGER service_set_car_id AFTER DELETE ON cars
DECLARE
CURSOR touched_rows is select * from service where car_id = null;
touched_row service%ROWTYPE;
BEGIN
OPEN touched_rows;
LOOP
FETCH touched_rows INTO touched_row;
EXIT WHEN touched_rows%NOTFOUND;
UPDATE service SET car_id = choose_ideal_car(touched_row.period,touched_row.day) WHERE service_id = touched_row.service_id;
END LOOP;
CLOSE touched_rows;
end;
/
For some reason my trigger is never fired.
I also tried creating triggers like this:
CREATE OR REPLACE TRIGGER service_set_car_id AFTER DELETE FROM cars FOR EACH ROW
CURSOR touched_rows is select * from service where car_id = :old.car_id;
...
Which is fired but throws 'mutating error' because function choose_ideal_car uses selects from both tables. Maybe the solution of this is to create duplicate of my cars table and select from it in my choose_ideal_car function instead of selecting from the cars table on which is my trigger defined, but that does not sounds good to me.
While I am writing this post I realized that even if my first trigger is fired it would not work correctly and throw the same 'mutating error'.
So in the end I have two questions:
1) Why is the first trigger never fired?
2) How to fix this mutating error and get all working correctly?
where car_id = null returns no rows, use where car_id is null instead