Invalid NEW or OLD specification error - sql

I'm trying to create trigger that checks if a phone number if in the (###) ###-#### format, if it this then nothing will happen, if is not then it will be fixed; however if there are more than 10 digits in the number then it will be turned to NULL.
Unfortunately I keep getting the Invalid NEW or OLD specification error in this trigger and I don't know why.
CREATE OR REPLACE TRIGGER phone_correction
BEFORE INSERT OR UPDATE OF vendor_phone
ON vendors
FOR EACH ROW
WHEN (NEW.vendor_phone != REGEXP_LIKE(vendor_phone, '^\(\d{3}\) \d{3}-\d{4}$'))
BEGIN
IF :NEW.vendor_phone != REGEXP_LIKE(vendor_phone, '^\D*(?:\d\D*){10}$')
THEN
:NEW.vendor_phone := null;
DBMS_OUTPUT.PUT_LINE( 'The phone number is bad so setting to null.');
ELSE
:NEW.vendor_phone := REGEXP_LIKE(vendor_phone, '^\(\d{3}\) \d{3}-\d{4}$');
END IF;
END;

There are several errors here; as others have said you need to explicitly use :new. and :old. to reference columns in your trigger, so REGEXP_LIKE(vendor_phone becomes REGEXP_LIKE(:new.vendor_phone.
However, there are some more fundamental errors.
As with the LIKE operator, REGEXP_LIKE() returns a Boolean. Thus, your statement:
IF :NEW.vendor_phone != REGEXP_LIKE(vendor_phone, '^\D*(?:\d\D*){10}$')
is actually IF <string> != <Boolean>, which'll never work.
Using DBMS_OUTPUT in a trigger isn't of any help to you unless you're going to be there to look at whatever logs you're keeping for every row that's been inserted, and then do something to correct whatever issues there are.
Silently removing data is bad practice, if you're going to change something then it's better to raise an error and let the calling code/user decide what to do instead.
If you don't want to let the calling code/user do anything and definitely want to NULL the column if it doesn't conform to a pattern then don't try and insert the data at all.
The ELSE condition in your IF statement is unnecessary, as :new.vendor_phone is already in the correct format.
Personally, I'd completely remove the trigger and add a constraint to check that the format in the column is the one in which you want:
SQL> alter table vendors
2 add constraint chk_vendors_phone
3 check (regexp_like(vendor_phone, '^\(\d{3}\) \d{3}-\d{4}$'));
Then, when trying to insert data it'll be successful if the format is correct and unsuccessful if the format is incorrect:
SQL> insert into vendors (vendor_phone)
2 values ('(123) 123-1234');
1 row created.
SQL> insert into vendors (vendor_phone)
2 values ('(123) 123-124');
insert into vendors (vendor_phone)
*
ERROR at line 1:
ORA-02290: check constraint (CHK_VENDORS_PHONE) violated
SQL>
You can then decide what to do with the phones that have errored. As I've stated above, if you definitely want to NULL the incorrectly formatted phones then only insert data which matches this pattern. If anyone touches the code the check constraint will ensure that the data is still in the correct format.
If you absolutely must use a trigger, then it can be simplified to something like the following:
create or replace trigger phone_correction
before insert or update of vendor_phone
on vendors
for each row
when (not regexp_like(new.vendor_phone, '^\(\d{3}\) \d{3}-\d{4}$'))
begin
:new.vendor_phone := null;
end;
This checks to see (using Boolean logic) whether the result of the REGEXP_LIKE() function is false. If it is, then it NULLs the phone. Here's an example of it working:
SQL> create table vendors (id number, vendor_phone varchar2(100));
Table created.
SQL> create trigger phone_correction
2 before insert or update of vendor_phone
3 on vendors
4 for each row
5 when (not regexp_like(new.vendor_phone, '^\(\d{3}\) \d{3}-\d{4}$'))
6 begin
7 :new.vendor_phone := null;
8 end;
9 /
Trigger created.
SQL> insert into vendors
2 values (1, '(123) 123-1234');
1 row created.
SQL> insert into vendors
2 values (2, '(123) 123-124');
1 row created.
SQL> select * from vendors;
ID VENDOR_PHONE
---------- --------------------
1 (123) 123-1234
2
SQL>
... instead of setting a phone number to null :new.vendor_phone := null; how would you make so it can automatically modify the phone number into the correct format? (###) ###-####
This is actually the example in the documentation for REGEXP_REPLACE(). To make this more extensible, I'd remove all non-numeric characters from the string and then attempt the transformation. In order to remove the non-numeric characters:
regexp_replace(vendor_phone, '[^[:digit:]]')
This means replace everything that's not in the character class [:digit:] with nothing. Then, to transform you can use sub-expressions as described in the documentation:
regexp_replace(regexp_replace(vendor_phone, '[^[:digit:]]')
, '^([[:digit:]]{3})([[:digit:]]{3})([[:digit:]]{4})$'
, '(\1) \2-\3')
This looks for 3 ({3}) digits twice and then 4 digits, splitting them into sub-expressions and then putting them in the correct format. There are many ways to do this, and this may not be the quickest, but it makes your intention most clear.
I would not do this in a trigger, do this when you insert into the table instead. Better, and if this is a client-side application, you should be ensuring that your numbers are in the correct format before you hit the database at all.

You have to specify the :NEW whenever you are using the column names. try this:
CREATE OR REPLACE TRIGGER phone_correction
BEFORE INSERT OR UPDATE OF vendor_phone
ON vendors
FOR EACH ROW
WHEN (NEW.vendor_phone != REGEXP_LIKE(NEW.vendor_phone, '^\(\d{3}\) \d{3}-\d{4}$'))
BEGIN
IF :NEW.vendor_phone != REGEXP_LIKE(:NEW.vendor_phone, '^\D*(?:\d\D*){10}$')
THEN
:NEW.vendor_phone := null;
DBMS_OUTPUT.PUT_LINE( 'The phone number is bad so setting to null.');
ELSE
:NEW.vendor_phone := REGEXP_LIKE(:NEW.vendor_phone, '^\(\d{3}\) \d{3}-\d{4}$');
END IF;
END;

You must specify the keywords NEW and OLD preceded by a colon (:) everywhere you are referring to the columns.
The only exception to this rule is the WHEN clause.
The NEW and OLD keywords, when specified in the WHEN clause, are not
considered bind variables, so are not preceded by a colon (:).
However, you must precede NEW and OLD with a colon in all references
other than the WHEN clause.
So, in your code, you must refer the new values in the conditions as :NEW.
REGEXP_LIKE(vendor_phone
Should be,
REGEXP_LIKE(:NEW.vendor_phone

Related

how to make sure that trigger generated value is being returned?

I have this INSERT query, which purpose is to insert the one row in my database.
Similarly I also have a INSERT query which insert multiple rows.
One of the columns in the table is generated after the values has been generated, since it combines a set of column values to construct a name. The name itself it generated from a Trigger, and its triggered After insert, since the column values has to exist for me to generate the name.
my problem now is when I insert one row or multiple rows, I want to know the the generated column value, but when I return it, it states its null?
#$"INSERT INTO registration_table (id, ...,)
VALUES (1,...,)
RETURNING row_id, name;";
which in return gives me an id the one I inserted, but the not actual name but instead I get null..
The trigger is pretty straight forward
CREATE TRIGGER name_insert_trigger
AFTER INSERT
ON registration_table
REFERENCING NEW TABLE AS new_inserts
FOR EACH STATEMENT
WHEN (pg_trigger_depth() = 0)
EXECUTE PROCEDURE registration_entry_name();
CREATE OR REPLACE FUNCTION registration_entry_name()
RETURNS trigger AS
$$
DECLARE
BEGIN
UPDATE registration_table
SET name = |Pattern| -- This one being the actual name generated..
FROM new_inserts
WHERE new_inserts.row_id = registration_table.row_id;
RETURN null;
END;
$$
LANGUAGE plpgsql;
but the insert query above does not return the name?
why not?
You actually need a BEFORE trigger, your data values will be there. The designation of Before and After very often causes misconceptions especially of row level triggers. The terms do not indicate their timing in relation to the DML. I have found it useful to think of them as "before final data values are set" and "after final data values are set" but both run before the invoking DML completes (for now we will bypass deferred triggers). Lets look at inserts. When the before row trigger fires the NEW row contains the values at that point for every column in the row, any value not specified in the statement will be null or contain the specified default if any. Before row triggers can can change any column. After row triggers cannot change columns, if present any change is ignored.
Your description and code imply you need to combine a couple columns to generate the content of another. Since you did not specify exactly that I will build an example and demo.
create table users ( usr_id integer generated always as identity
, lname text not null
, fname text not null
, full_name text not null
) ;
create or replace
function users_bir()
returns trigger
language plpgsql
as $$
begin
if new.full_name is null
then
new.full_name = trim(new.fname) || ' ' || trim(new.lname);
end if;
return new;
end;
$$;
create trigger users_bir_trg
before insert on users
for each row
execute procedure users_bir();
insert into users(fname, lname)
values ( 'George', 'Henery')
, ( 'Samatha', 'van Horm');
insert into users(fname, lname, full_name)
values ( 'Wacky', 'Warriors','Not so tough guys');
This setup allows the full_name to be specified or generated. If only generation is desired remove the IF leaving only the assignment statement. Even better if you have Postgres 12 or higher just define the the column as a generated column. This is also in the demo.

How to successfully reference another table before insert with a trigger

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.

How can i do an insert into a table of my DB that has one attribute of ADT type? with Oracle Live SQL

I have created a table for seeing how many people could die from COVID-19 in Latin country's for that i created an ADT structure which have two attributes probabilidad_fallecidos that means probability to death and cantidad_infectados that is the quantity of infected per country, The part i'm having problems is when i try to do an insert says ORA-00947: not enough values
I'm very new at this, this is my first try
Below i will let my ADT structure,my function, my table and my try of insert
ADT
CREATE OR REPLACE TYPE infectados AS OBJECT(
cantidad_infectados number,
probabilidad_fallecidos number,
STATIC FUNCTION cantidad_fallecidos(cantidad_infectados number,probabilidad_fallecidos number) RETURN number
);
Function cantidad_fallecidos
CREATE OR REPLACE TYPE BODY infectados IS
STATIC FUNCTION cantidad_fallecidos(cantidad_infectados number,probabilidad_fallecidos number) RETURN number
IS numero1 number(1);
BEGIN
IF cantidad_infectados > probabilidad_fallecidos*cantidad_infectados THEN
RETURN (probabilidad_fallecidos*cantidad_infectados);
ELSE
RAISE_APLICATION_ERROR(-2000,'Error: cantidad_infectados es menor a la probabilidad de fallecidos');
END IF;
END;
END;
Creation of my table
CREATE TABLE Vnzla_infectado(
vnzlaInf_id NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY,
num_infectados infectados
);
Try of insert
INSERT INTO Vnzla_infectado
VALUES (infectados(100,0.1,infectados.cantidad_fallecidos(100,0.1)));
You are getting the error ORA-00947: not enough values because you are supplying one value to insert into a table with two columns, and you are not specifying which column you are trying to insert into so Oracle thinks you are inserting into all columns.
Your vnzlaInf_id column may be generated by an IDENTITY, but it looked to the database as if you were attempting to insert a value into that column and nothing into the num_infectados column, hence the error about not enough values.
So the first thing you need to do is to modify the INSERT statement to tell the database which column you want to insert into:
INSERT INTO Vnzla_infectado (num_infectados)
VALUES ...
I wrote "first thing" because there is another problem with your INSERT statement. If you add that column name, you get another error, ORA-02315: incorrect number of arguments for default constructor. This is because your type constructor has two arguments, but you are specifying three. One way to fix it is to get rid of the third argument:
INSERT INTO Vnzla_infectado (num_infectados)
VALUES (infectados(100,0.1));
This INSERT statement runs successfully.
Alternatively, you may want to add another field to your type:
CREATE OR REPLACE TYPE infectados AS OBJECT(
cantidad_infectados number,
probabilidad_fallecidos number,
your_new_field_name_here number,
STATIC FUNCTION cantidad_fallecidos(cantidad_infectados number,probabilidad_fallecidos number) RETURN number
);
If you are going to change the type, you will have to drop the table first and recreate it afterwards. After doing this, your original INSERT statement runs fine.
While I'm here, there are some other problems I noticed with your static function cantidad_fallecidos. Firstly, there is a typo in RAISE_APLICATION_ERROR, it should be RAISE_APPLICATION_ERROR - you're missing one of the Ps. Secondly, the argument -2000 will get rejected by Oracle: it will complain with ORA-21000: error number argument to raise_application_error of -2000 is out of range if you attempt to raise your custom error. I guess you meant to use -20000 for the error number instead. Thirdly, the condition
cantidad_infectados > probabilidad_fallecidos*cantidad_infectados
looks a bit odd to me. Provided cantidad_fallecidos is greater than zero, then it is equivalent to
1 > probabilidad_fallecidos
Also, are you sure you need to use > rather than >=? This leads to some odd behaviour in unusual cases: if cantidad_infectados is zero, your condition will never be true and your custom error will be raised whatever probabilidad_fallecidos is. To me it makes more sense to validate that probabilidad_fallecidos is between 0 and 1.

Check if VARCHAR2 contains only alphabets using trigger

I need to write such a trigger that will check name of the person and will print out his/her id if those people have any digits in theirs names.
What I have by now:
set SERVEROUTPUT ON
create or replace trigger BeforeUpdate
Before insert on customer
for each row
declare
n varchar2(10);
counter number;
nextR number:=0;
begin
select count(id_customer) into counter from customer;
LOOP
nextR:= nextR +1;
select cname into n from customer where id_customer = nextR;
if n not like '%[0-9]%' then
DBMS_OUTPUT.PUT_LINE(nextR || ' has incorrect name');
end if;
exit when nextR = counter;
end loop;
end;
/
It compiles and when I am trying to fire this trigger it do nothing.
I will be grateful for any help.
There are a couple of problems in your code:
using dbms_output in a trigger doesn't really make sense; usually, the INSERT will be performed by client code that doesn't handle the console output.
The sensible thing is to raise an exception instead.
You don't need to perform a SELECT in your trigger code. In fact, doing so will usually either be superfluous or raise a mutating table error. Instead, use :new and :old to refer to the values of the row that was inserted
(minor) naming a before insert trigger BeforeUpdate is somewhat confusing
use a regular expression for testing this business rule (seriously; regexes rule for this kind of thing)
Altogether, here's the fixed version (untested, I don't have an Oracle instance available for testing right now):
create or replace trigger TR_BI_CUSTOMER
Before insert on customer
for each row
begin
if regexp_like(:new.name, '.*[0-9].*') then
raise_application_error(-20001, 'Incorrect name: ' || :new.name);
end if;
end;
Use regular expression to get your result.
In your case, if you get a digit in n, your if clause should be executed.
So,
if regexp_replace(n,'[^[:digit:]]') IS NOT NULL then
DBMS_OUTPUT.PUT_LINE(nextR || ' has incorrect name');
end if
It seems you are also attempting to use a regular expression for digit. However, what your code is searching is for a string that has [0-9] in it. Like Bat[0-9]Man, which is not your desired result.
In my code, whatever expression is not digit in the given name is being replaced. If the name does not contain any digits, the regular expression would return null. If there is any digit at any place,the expression would return those digits.
You could analyse the following query for better grasping of what is happening here:
select regexp_replace(cname,'[^[:digit:]]') OUTP, cname from customer;
EDIT :
This is not how you write a trigger !
The trigger will be fired each time an insert is going to take place. You don't need the counter. You need to use :NEW reference
set SERVEROUTPUT ON
create or replace trigger update or
Insert on customer
for each row
begin
if regexp_replace(:NEW.cname,'[^[:digit:]]') IS NOT NULL then
DBMS_OUTPUT.PUT_LINE(nextR || ' has incorrect name');
end if;
end;
/
This is a job for REGEXP_LIKE()! The regex of '\d' matches a number.
SQL> with tbl(id, name) as (
select 1, 'Batman' from dual union
select 2, 'Robin1' from dual union
select 3, 'Supe4rman' from dual union
select 4, '3Joker' from dual
)
select id, name bad_name
from tbl
where regexp_like(name, '\d');
ID BAD_NAME
---------- ---------
2 Robin1
3 Supe4rman
4 3Joker
SQL>
If your goal is to strip out the digits on the way in (but be careful, a company really could have a number in the name like Level3 Communications or 3Com, if it's a person its less likely but these days who knows!) This is untested:
CREATE OR REPLACE TRIGGER customer_bu
BEFORE INSERT OR UPDATE
ON customer
REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
BEGIN
-- If the new name contains a digit, strip it.
if regexp_like(:new.name, '\d') then
:new.name := regexp_replace(:new.name, '\d', NULL);
end if;
END customer_bu;
/

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.