I'm quite new to PL/SQL, sorry if the question is obvious
According to the TRIGGER documentation, there is a WHEN ( condition ) for triggers. I wanted to use an exists condition, which requires a subquery, however, I have the following error :
ORA-02251
00000 - "subquery not allowed here"
*Cause: Subquery is not allowed here in the statement.
*Action: Remove the subquery from the statement.
What did I miss?
My condition is the following :
CREATE OR REPLACE TRIGGER mytrigger AFTER UPDATE OF column ON THIS_TABLE
FOR EACH ROW
WHEN (NEW.status = 'approved' AND EXISTS (
SELECT * FROM JUNCTION_TABLE WHERE THIS_TABLE_ID=NEW.this_table_id AND OTHER_TABLE_ID = 'SOMETHING'))
DECLARE
BEGIN
END;
I want to check whether the row is associated to a given value, which I can only find in a junction table.
I could surely do this in the PL/SQL part of the trigger, but :
it is related to the trigger rather than the business logic in itself
I'd like to understand what I missed in the documentation and why it is not possible.
If another condition might do this, I'm also interested.
I would write this with the conditional element within the trigger itself, something like
CREATE OR REPLACE TRIGGER mytrigger AFTER UPDATE OF column ON THIS_TABLE
FOR EACH ROW
WHEN (NEW.status = 'approved')
DECLARE
BEGIN
IF EXISTS (
SELECT * FROM JUNCTION_TABLE WHERE THIS_TABLE_ID=NEW.this_table_id AND OTHER_TABLE_ID = 'SOMETHING'))
...
END;
I don't know which Oracle documentation was used. Though the Oracle 10.2 documentation doesn't mention this, in the Oracle 11.1 documentation the limitation is mentioned:
The expression in a WHEN clause must be a SQL expression, and it cannot include a subquery. You cannot use a PL/SQL expression (including user-defined functions) in the WHEN clause.
There's no alternative I can think of at the moment to checking the condition inside the trigger code, as mentioned.
Related
I have a table Content like this:
id | text | date | idUser → User | contentType
And another table Answer:
idAnswer → Content | idQuestion → Content | isAccepted
I want to ensure that the Answer's date is bigger than the Question's date. A question is a Content with contentType = 'QUESTION'.
I tried to solve this with the following trigger, but when I try to insert an Answer there's an error:
ERROR: record "new" has no field "idanswer"
CONTEXT: SQL statement "SELECT (SELECT "Content".date FROM "Content" WHERE "Content".id = NEW.idAnswer) < (SELECT "Content".date FROM "Content" WHERE "Content".id = NEW.idQuestion)"
PL/pgSQL function "check_valid_date_answer" line 2 at IF
Trigger:
CREATE TRIGGER check_valid_answer
AFTER INSERT ON "Answer"
FOR EACH ROW EXECUTE PROCEDURE check_valid_date_answer();
Trigger function:
CREATE FUNCTION check_valid_date_answer() RETURNS trigger
LANGUAGE plpgsql
AS $$BEGIN
IF (SELECT "Content".date FROM "Content"
WHERE "Content".id = NEW.idAnswer)
< (SELECT "Content".date FROM "Content"
WHERE "Content".id = NEW.idQuestion)
THEN
RAISE NOTICE 'This Answer is an invalid date';
END IF;
RETURN NEW;
END;$$;
So, my question is: do I really need to create a trigger for this? I saw that I can't use a CHECK in Answer because I need to compare with an attribute of another table. Is there any other (easier/better) way to do this? If not, why the error and how can I solve it?
Your basic approach is sound. The trigger is a valid solution. It should work except for 3 problems:
1) Your naming convention:
We would need to see your exact table definition to be sure, but the evidence is there. The error message says: has no field "idanswer" - lower case. Doesn't say "idAnswer" - CaMeL case. If you create CaMeL case identifiers in Postgres, you are bound to double-quote them everywhere for the rest of their life.
Are PostgreSQL column names case-sensitive?
2) Abort violating insert
Either raise an EXCEPTION instead of a friendly NOTICE to actually abort the whole transaction.
Or RETURN NULL instead of RETURN NEW to just abort the inserted row silently without raising an exception and without rolling anything back.
I would do the first. This will probably fix the error at hand and work:
CREATE FUNCTION trg_answer_insbef_check()
RETURNS trigger AS
$func$
BEGIN
IF (SELECT c.date FROM "Content" c WHERE c.id = NEW."idAnswer")
< (SELECT c.date FROM "Content" c WHERE c.id = NEW."idQuestion") THEN
RAISE EXCEPTION 'This Answer is an invalid date';
END IF;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
The proper solution is to use legal, lower case names exclusively and avoid such problems altogether. That includes your unfortunate table names as well as the column name date, which is a reserved word in standard SQL and should not be used as identifier - even if Postgres allows it.
3) Should be a BEFORE trigger
CREATE TRIGGER insbef_check
BEFORE INSERT ON "Answer"
FOR EACH ROW EXECUTE PROCEDURE trg_answer_insbef_check();
You want to abort invalid inserts before you do anything else.
Of course you will have to make sure that the timestamps table Content cannot be changed or you need more triggers to make sure your conditions are met.
The same goes for the fk columns in Answer.
I would approach this in a different way.
Recommendation:
use a BEFORE INSERT trigger if you want to change data before
inserting it
use a AFTER INSERT trigger if you have to do additional
work
use a CHECK clause if you have additional data consistency requirements.
So write a sql function that checks the condition that one date be earlier than the other, and add the check constraint. Yes, you can select from other tables in your function.
I wrote something similar (complex check) in answer to this question on SO.
I am writing an INSTEAD OF UPDATE trigger and I want to identify what columns has been given to the WHERE clause of the UPDATE statement that triggers the trigger.
For example,
Let's say that we have the table below
table_name
--COL1
--COL2
--COL3
--COL4
I want, when an update is performed
e.g.UPDATE table_name SET COL1=VAL1,COL2=VAL2 WHERE COL3=VAL3
to be able to say in my trigger
CREATE or replace TRIGGER DEVICES_VIEW_TR
INSTEAD OF UPDATE ON DEVICES_VW
BEGIN
IF (COL3 has been given in the where clause) THEN
variable=getValueOf(COL3);
ELSEIF (COL4 has been given in the where clause) THEN
variable=getValueOf(COL4);
END IF;
END;
/
Can this be done?
Thanks
You can use the UPDATING('column name') in your trigger:
-- in INSTEAD OF trigger body:
IF updating('COL1') THEN
-- some operation
END IF;
Check this for an example: Example of using UPDATING
You could use the NEW and OLD pseudorecords and run a comparison of the values
if :NEW.COL3 <> :OLD.COL3 THEN ...
Triggers don't know anything about the statement that invoked them, so you'll have to use some kind of out-of-band signalling, e.g. change your application to set some globals in a database package, or use an application context.
I have a weird problem with a trigger in HSQLDB. I've checked the syntax several times but I haven't found any mistakes. I now think that it may be impossible to create a subquery inside a trigger.
This is the code:
create trigger activate_member after update on member
REFERENCING OLD AS old NEW AS new
for each row
begin atomic
IF new.active = FALSE AND old.active = TRUE then
insert into member_inactive
(
member,
since,
until
)
values (
new.id,
(select since from member_active where
member = new.id AND since = (select max(since) from member_active where
member = new.id group by member)),
curdate()
);
ELSEIF new.active = TRUE AND old.active = FALSE then
insert into member_active (member, since) values
(new.id, curdate());
end if;
end;
I think I've made everything correct: I have semicolons on every query inside the IF and ELSIEF statements, and trailing semicolon after end if.
But I still get this error message:
SEVERE SQL Error at 'database.sql' line 125:
"create trigger activate_member after update on member
REFERENCING OLD AS old NEW AS new
for each row
begin atomic
IF new.active = FALSE AND old.active = TRUE then
insert into member_inactive
(
member,
since,
until
)
values (
new.id,
(select since from member_active where member = new.id AND since = (select max(since) from member_active where member = new.id group by member)),
curdate()
)"
unexpected end of statement: required: ; : line: 17
org.hsqldb.cmdline.SqlTool$SqlToolException
But where I should add a semicolon? I think the problem is the subquery inside the insert, but that makes no sense.
Probably the begin atomic is problematic. But I see no other way to have an if-else statement inside a trigger without it!
Have you tried adding the ";" after "curdate()"?
Hopefully that helps you with that problem.
You may get a problem related to the use of new.id in the nested subquery in the VALUES clause of the INSERT statements.
Create a variable and assign the computed "SINCE" column to this, then use this variable in the VALUES list.
But at second glance, it looks the issue may relate to the software you use to execute the script, which cuts off the statement when it finds a semicolon. If you are using SqlTool, please use the latest version of the software and check the Guide on using RAW mode to avoid this issue.
i found now whats caused this problem: hsqldb only has begin atomic blocks inside a trigger.
Inside a begin atomic block i cannot use insert statements...
i now changed it to two triggers with where (statement) clauses, this works also
I'm new in oracle and i don't know what is wrong with this trigger:
CREATE OR REPLACE TRIGGER "propuesta_casas"
BEFORE INSERT ON "PROPUESTA_TIENDA_BARRIO"
FOR EACH ROW
WHEN (new."CASASCAL" IS NULL)
BEGIN
SELECT PROPUESTA.CASAS
INTO :new."CASASCAL"
FROM PROPUESTA WHERE PROPUESTA.IDPROPUESTA=new.IDPROPUESTA ;
END;
/
Error:
PL/SQL: ORA-00904: "NEW"."IDPROPUESTA": identifider not valid
Not sure why the accepted answer has been accepted as neither it nor the attached comments seem to address the obvious issue in the posted code.
In a trigger body we reference values in the inserted row with the :NEW code word. The posted code lacks the colon when it references the column in the WHERE clause. This is what is needed:
CREATE OR REPLACE TRIGGER "propuesta_casas"
BEFORE INSERT ON "PROPUESTA_TIENDA_BARRIO"
FOR EACH ROW
WHEN (new."CASASCAL" IS NULL)
BEGIN
SELECT PROPUESTA.CASAS
INTO :new."CASASCAL"
FROM PROPUESTA
WHERE PROPUESTA.IDPROPUESTA=:new.IDPROPUESTA ;
END;
/
Incidentally, watch out for using lower case in double quotes when creating objects.
By default all Oracle names are stored in the data dictionary in upper case, but the SQL statements are case insensitive. So the following two statments refer to the same object:
select * from emp
/
select * from EMP
/
However, if we create our object with a name in mixed case or lower case and put it in double quotes it is stored in the data dictionary with that exact case. This means we have to use that exact case whenever we reference the object, in double quotes. So if we created a table with all lower case ...
create table "emp" ...
... then this statement will fail:
select * from emp
/
It has to be
select * from "emp"
/
Of course if we already have a table called EMP then the first statement would have succeeded, if would just have selected from a different table.
In the case of triggers we generally don't refer to them by name. But we have to use the case whenever we look up the trigger in the data dictionary:
select status
from user_triggers
where trigger_name = 'propuesta_casas'
/
From what you described:
Try to recompile the trigger and see what happens...
A trigger becomes invalid if the base object (ex..table) becomes invalid or altered and the trigger refers to the affected table.
Excuting the line of SQL:
SELECT *
INTO assignment_20081120
FROM assignment ;
against a database in oracle to back up a table called assignment gives me the following ORACLE error:
ORA-00905: Missing keyword
Unless there is a single row in the ASSIGNMENT table and ASSIGNMENT_20081120 is a local PL/SQL variable of type ASSIGNMENT%ROWTYPE, this is not what you want.
Assuming you are trying to create a new table and copy the existing data to that new table
CREATE TABLE assignment_20081120
AS
SELECT *
FROM assignment
First, I thought:
"...In Microsoft SQL Server the
SELECT...INTO automatically creates
the new table whereas Oracle seems to
require you to manually create it
before executing the SELECT...INTO
statement..."
But after manually generating a table, it still did not work, still showing the "missing keyword" error.
So I gave up this time and solved it by first manually creating the table, then using the "classic" SELECT statement:
INSERT INTO assignment_20081120 SELECT * FROM assignment;
Which worked as expected. If anyone come up with an explanaition on how to use the SELECT...INTO in a correct way, I would be happy!
You can use select into inside of a PLSQL block such as below.
Declare
l_variable assignment%rowtype
begin
select *
into l_variable
from assignment;
exception
when no_data_found then
dbms_output.put_line('No record avialable')
when too_many_rows then
dbms_output.put_line('Too many rows')
end;
This code will only work when there is exactly 1 row in assignment. Usually you will use this kind of code to select a specific row identified by a key number.
Declare
l_variable assignment%rowtype
begin
select *
into l_variable
from assignment
where ID=<my id number>;
exception
when no_data_found then
dbms_output.put_line('No record avialable')
when too_many_rows then
dbms_output.put_line('Too many rows')
end;
Though this is not directly related to the OP's exact question but I just found out that using a Oracle reserved word in your query (in my case the alias IN) can cause the same error.
Example:
SELECT * FROM TBL_INDEPENTS IN
JOIN TBL_VOTERS VO on IN.VOTERID = VO.VOTERID
Or if its in the query itself as a field name
SELECT ..., ...., IN, ..., .... FROM SOMETABLE
That would also throw that error. I hope this helps someone.
If you backup a table in Oracle Database. You try the statement below.
CREATE TABLE name_table_bk
AS
SELECT *
FROM name_table;
I am using Oracle Database 12c.
Late answer, but I just came on this list today!
CREATE TABLE assignment_20101120 AS SELECT * FROM assignment;
Does the same.