How to successfully reference another table before insert with a trigger - sql

I'm trying to create a trigger to validate if a new entry in the table registraties (registrations) contains a valid MNR (employee number) but I'm getting stuck on the part where I'm referencing the table medewerkers (employees).
Could someone help me out?
CREATE OR REPLACE TRIGGER t_MNRcontrole
BEFORE INSERT OR UPDATE
ON registraties
DECLARE
MNR_medewerkers number (SELECT MNR FROM MEDEWERKERS);
FOR EACH ROW
BEGIN
IF :new.MNR <> MNR_medewerkers
THEN raise_application_error(-20111, 'Medewerker niet herkend!');
END IF;
END;
Error message received is
ORA-24344: success with compilation error

The PL/SQL assignment operator is :=, or select x into y from z to populate from a SQL query.
FOR EACH ROW is part of the trigger spec, not the PL/SQL code.
If :new.mnr is not present in the parent table, you will get a no_data_found exception, not a mismatched variable.
It's good practice for error messages to include details of what failed.
In programming, we use indentation to indicate code structure.
A fixed version would be something like:
create or replace trigger trg_mnrcontrole
before insert or update on registraties
for each row
declare
mnr_medewerkers medewerkers.mnr%type;
begin
select mw.mnr into mnr_medewerkers
from medewerkers mw
where mw.mnr = :new.mnr;
exception
when no_data_found then
raise_application_error(-20111, 'Medewerker '||:new.mnr||' niet herkend!');
end;
However, we can implement this kind of check better using a foreign key constraint, for example:
alter table registraties add constraint registraties_mw_fk
foreign key (mnr) references medewerkers.mnr;

MNR_medewerkers number (SELECT MNR FROM MEDEWERKERS);
will always fail because its not a NUMBER, unless your table happens to only have one single entry and even then I am not sure PLSQL will allow it to pass.
The more standard case for this would be to first declare the number, then in the codeblock you do a SELECT INTO along with a WHERE clause where you make sure to only pick one specific row from the table. Then you can compare that number with the new one.
If however you are not trying to compare to one specific row, but are instead checking if the entry exists in that table.
BEGIN
SELECT 1
INTO m_variable
FROM table
WHERE MNR = :new.MNR;
EXCEPTION
WHEN TOO_MANY_ROWS THEN
m_variable = 1;
WHEN OTHERS THEN
m_variable = 0;
END;
Declare the m_variable beforehand, and then check if its 0 then report the error.
The too_many_rows is in case there is more than one row in the table with this MNR, and the OTHERS is there for the NO_DATA_FOUND, but I use OTHERS to handle everything else that could happen but probably wont.
Btw this is a code block to be included within the main code block, so between your BEGIN and IF, then just change the IF to check if the variable is 0.

Related

Error(7,56): PLS-00049: bad bind variable 'RACE.Race_location'

I'm trying to create a trigger that references a table within my sql code. If there is a more simple way than using a trigger please let me know :) But i seem to be getting this error and i can't for the life of me work out why. Everything is named correctly, i have double and tripple checked.
CREATE OR REPLACE TRIGGER RACEDATECHECK
BEFORE INSERT OR UPDATE ON RACE
REFERENCING NEW AS RACE
FOR EACH ROW
DECLARE meet_end_date DATE;
BEGIN
SELECT EndDate INTO meet_end_date FROM MEETING WHERE :RACE."Race_location" =
MEETING."Meeting_Location" AND :RACE.Meeting_start = MEETING.StartDate;
IF :RACE."Race_Date" > meet_end_date
THEN
RAISE_APPLICATION_ERROR(-20000, 'Error, race date must
fit between meeting parameters');
END IF;
END;

Compound trigger: collecting mutating rows into nested table

I have two tables in my project: accounts and transactions (one-to-many relationship). In every transaction I store the balance of the associated account (after the transaction is executed). Additionally in every transaction I store a value of the transaction.
So I needed a trigger fired when someone adds new transaction. It should check whether new account balance will be correct (old account balance + transaction value = new account balance stored in transaction).
So I was suggested, I should use a compound trigger which would:
in before each row section: save a row's PK (made of two columns) somewhere,
in after statement section: check whether all inserted transactions where correct.
Now I can't find anywhere how could I implement the first point.
What I already have:
CREATE OR REPLACE TRIGGER check_account_balance_is_valid
FOR INSERT
ON Transactions
COMPOUND TRIGGER
TYPE Modified_transactions_T IS TABLE OF Transactions%ROWTYPE;
Modified_transactions Modified_transactions_T;
BEFORE STATEMENT IS BEGIN
Modified_transactions := Modified_transactions_T();
END BEFORE STATEMENT;
BEFORE EACH ROW IS BEGIN
Modified_transactions.extend;
Modified_transactions(Modified_transactions.last) := :NEW;
END BEFORE EACH ROW;
AFTER STATEMENT IS BEGIN
NULL; -- I will write something here later
END AFTER STATEMENT;
END check_account_balance_is_valid;
/
However, I got that:
Warning: execution completed with warning
11/58 PLS-00049: bad bind variable 'NEW'
Could someone tell me, how to fix it? Or maybe my whole "compound trigger" idea is wrong and you have better suggestions.
Update 1
Here is my ddl script: http://pastebin.com/MW0Eqf9J
Maybe try this one:
TYPE Modified_transactions_T IS TABLE OF ROWID;
Modified_transactions Modified_transactions_T;
BEFORE STATEMENT IS BEGIN
Modified_transactions := Modified_transactions_T();
END BEFORE STATEMENT;
BEFORE EACH ROW IS BEGIN
Modified_transactions.extend;
Modified_transactions(Modified_transactions.last) := :NEW.ROWID;
END BEFORE EACH ROW;
or this
TYPE PrimaryKeyRecType IS RECORD (
Col1 Transactions.PK_COL_1%TYPE, Col2 Transactions.PK_COL_2%TYPE);
TYPE Modified_transactions_T IS TABLE OF PrimaryKeyRecType;
...
Modified_transactions(Modified_transactions.last) := PrimaryKeyRecType(:NEW.PK_COL_1, :NEW.PK_COL_2);
Your immediate problem is that :new is not a real record so it is not of type Transactions%ROWTYPE. If you're really going to go down this path, you would generally want to declare a collection of the primary key of the table
TYPE Modified_transactions_T IS TABLE OF Transactions.Primary_Key%TYPE;
and then put just the primary key in the collection
BEFORE EACH ROW IS BEGIN
Modified_transactions.extend;
Modified_transactions(Modified_transactions.last) := :NEW.Primary_Key;
END BEFORE EACH ROW;
The fact that you are trying to work around a mutating table exception in the first place, however, almost always indicates that you have an underlying data modeling problem that you should really be solving. If you need to query other rows in the table in order to figure out what you want to do with the new rows, that's a pretty good indication that you have improperly normalized your data model and that one row has some dependency on another row in the same table rather than being an autonomous fact. Fixing the data model is almost always preferrable to working around the mutating table exception.

Writing an SQL trigger to find if number appears in column more than X times?

I want to write a Postgres SQL trigger that will basically find if a number appears in a column 5 or more times. If it appears a 5th time, I want to throw an exception. Here is how the table looks:
create table tab(
first integer not null constraint pk_part_id primary key,
second integer constraint fk_super_part_id references bom,
price integer);
insert into tab values(1,NULL,100), (2,1,50), (3,1,30), (4,2,20), (5,2,10), (6,3,20);
Above are the original inserts into the table. My trigger will occur upon inserting more values into the table.
Basically if a number appears in the 'second' column more than 4 times after inserting into the table, I want to raise an exception. Here is my attempt at writing the trigger:
create function check() return trigger as '
begin
if(select first, second, price
from tab
where second in (
select second from tab
group by second
having count(second) > 4)
) then
raise exception ''Error, there are more than 5 parts.'';
end if;
return null;
end
'language plpgsql;
create trigger check
after insert or update on tab
for each row execute procedure check();
Could anyone help me out? If so that would be great! Thanks!
CREATE FUNCTION trg_upbef()
RETURN trigger as
$func$
BEGIN
IF (SELECT count(*)
FROM tab
WHERE second = NEW.second ) > 3 THEN
RAISE EXCEPTION 'Error: there are more than 5 parts.';
END IF;
RETURN NEW; -- must be NEW for BEFORE trigger
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER upbef
BEFORE INSERT OR UPDATE ON tab
FOR EACH ROW EXECUTE procedure trg_upbef();
Major points
Keyword is RETURNS, not RETURN.
Use the special variable NEW to refer to the newly inserted / updated row.
Use a BEFORE trigger. Better skip early in case of an exception.
Don't count everything for your test, just what you need. Much faster.
Use dollar-quoting. Makes your live easier.
Concurrency:
If you want to be absolutely sure, you'll have to take an exclusive lock on the table before counting. Else, concurrent inserts / updates might outfox each other under heavy concurrent load. While this is rather unlikely, it's possible.

PL/PgSQL: Catching unique violation exceptions and inserting from 'record' variables

I've got a nightly archive job that sweeps existing rows from a records table into records_archive:
INSERT INTO records_archive
SELECT * FROM records WHERE id > 555 AND id <= 556;
For reasons that are outside the scope of this question, there is a column -- master_guid -- in both records and records_archive that has a UNIQUE index, and I can't change that. Rebuilding the index into a non-unique one is off the table for reasons out of my control. So, in principle, every master_guid value is supposed to be unique. There are some bad client implementations out there that sometimes fail to generate unique enough GUIDs, though, which results in a collision when we attempt to insert a record into records_archive that with a master_guid value that already exists in records_archive.
I can't fix the clients, so I need to work around it. The way to do that is to catch the unique_violation exception, modify the GUID (adding some random characters to it), and attempt to re-insert.
I can't just wrap the above-mentioned INSERT query in a stored procedure and catch the unique_violation exception, because the whole query is one transaction. I need row-level access. So, I wrote a stored procedure to iterate over each row and catch a unique_violation exception for that row individually:
CREATE OR REPLACE FUNCTION archive_records(start_id bigint,
end_id bigint)
RETURNS void
AS $$
DECLARE
_r record;
BEGIN
FOR _r IN
SELECT * FROM records WHERE id > start_id AND id <= end_id
LOOP
BEGIN
INSERT INTO records_archive VALUES (_r.*);
EXCEPTION WHEN unique_violation THEN
-- Manipulate the _r.master_guid value, add some random
-- numbers to it or whatever, and attempt reinsertion.
END;
END LOOP;
END;
$$ LANGUAGE 'plpgsql';
The problem is that records (and records_archive) are insanely wide tables. I don't want to explicitly enumerate every single column to be copied from _r to records_archive, not only because I'm lazy, but because this stored procedure would become a dependency in any future column changes in those tables.
The problem I've got is that this doesn't work, syntactically or conceptually:
INSERT INTO records_archive VALUES (_r.*);
Neither do any of these:
INSERT INTO records_archive _r;
INSERT INTO records_archive _r.*;
Is there a way to pull this off? If not, is there a better way to accomplish what I'm trying to accomplish?
I would create a BEFORE INSERT Trigger on records_archive. Do a select on the records_archive for the NEW.master_guid, and if it exists, manipulate it to add your random numbers.
You'd probably want a loop around the check to ensure the modified master_guid still didn't exist before went ahead with the insertion.

how to create a trigger in oracle which will restrict insertion and update queries on a table based on a condition

I have account table as this--
create table account
(
acct_id int,
cust_id int,
cust_name varchar(20)
)
insert into account values(1,20,'Mark');
insert into account values(2,23,'Tom');
insert into account values(3,24,'Jim');
I want to create a trigger which will ensure that no records can be inserted or update in account table having acct_id as 2 and cust_id as 23.
My code is --
create trigger tri_account
before insert or update
on account
for each row
begin
IF (:new.acct_id == 2 and :new.cust_id == 23) THEN
DBMS_OUTPUT.PUT_LINE('No insertion with id 2 and 23.');
rollback;
END IF;
end;
so this trigger is created , but with compilation error.
now when I insert any record with acct_id as 2 and cust_id as 23,it doesent allow.
But I get an error saying
ORA-04098: trigger 'OPS$0924769.TRI_ACCOUNT' is invalid and failed re-validation
I don't understand this.I also want to show a message that dis insertion is not possible.
please Help...
The equality operator in Oracle is =, not ==.
You cannot commit or rollback in a trigger. You can throw an exception which causes the triggering statement to fail and to be rolled back (though the existing transaction will not necessarily be rolled back).
It does not appear that this trigger compiled successfully when you created it. If you are using SQL*Plus, you can type show errors after creating a PL/SQL object to see the compilation errors.
You should never write code that depends on the caller being able to see the output from DBMS_OUTPUT. Most applications will not so most applications would have no idea that the DML operation failed if your trigger simply tries to write to the DBMS_OUTPUT buffer.
Putting those items together, you can write something like
create trigger tri_account
before insert or update
on account
for each row
begin
IF (:new.acct_id = 2 and :new.cust_id = 23) THEN
raise_application_error( -20001, 'No insertion with id 2 and 23.');
END IF;
end;
A trigger is more flexible, but you can also accomplish this through the use of a CHECK CONSTRAINT:
ALTER TABLE account ADD CONSTRAINT check_account CHECK ( acct_id != 2 OR cust_id != 23 )
ENABLE NONVALIDATE;
The NONVALIDATE clause will ensure that the check constraint does not attempt to validate existing data, though it will validate all future data.
Hope this helps.
IF (:new.acct_id = 2 and :new.cust_id = 23) THEN
must be OR, not and.
While using conditional checks you don't need to use colons (:). This will always cause errors.
Note: Exclude the colon only in cases where condition checking is performed.