What is the right prototype for my SQL code? - sql

I am new in learning SQL and I am trying to apply a trig on those two tables by using SQLcl:
prodc
( PRODC_NAME, DISCOUNTED CHAR(1) DEFAULT 'N',
PK PRIMARY KEY (PRODC_NAME));
peopleOrder
( ORDERID, PRODC_NAME,
PRIMARY KEY (ORDERID, PRODC_NAME),
FOREIGN KEY (PRODC_NAME) REFERENCES PRODC (PRODC_NAME));
I want my trig to do the following:
when I update or insert rows to peopleOrder, the trig should check if a row's product_name has value 'N' in the discounted column that is located in the product table; if it has no 'N' value, it should show an error message.
I tried many ways and the following trig, but when I insert rows, the trig seems to not be working and have no effect on the rows:
CREATE OR REPLACE TRIGGER constraint_1
After UPDATE OR INSERT on peopleOrder
for each row
begin
if not (:new.prodc_name in
(select prodc_name from prodc where discounted = 'N' )) then
RAISE_ERROR(-30001, 'No product can be ordered
!');
END IF;
INSERT INTO peopleOrder VALUES (:new.ORDERID, :new.PRODC_NAME);
END;
/
My insert command is:
INSERT INTO peopleOder (ORDERID, PRODC_NAME)
VALUES (251, 'Puton');
and 'Puton' has value 'N' in the discounted column in the prodc table.

The first thing you need is to understand what a trigger is. It is a piece of code that runs as part of the statement causing it to fire. For this reason you cannot reference the table causing it to fire. Normally that is completely unnecessary anyway. Every row trigger has access to to 2 rows: a copy of the existing data (old) and the resulting data (new). Update having both filled and Insert/Delete just new/old as appropriate for the action. Before statement triggers can modify the new data, after triggers cannot. Those rows allow you to process the columns in the table without DML or select on it. (However you cannot 'scan' the table). At any level a trigger may raise an exception which halts the entire process and the invoking statement. If no exception is raised then processing the invoking proceeds. For a more complete description see PL/SQL Language Reference or the version of Oracle you are running.
In this case your trigger breaks down to a single if
statement.
-- Revised. Orig missed that flag comming from different table.
create or replace trigger constraint_1
before update or insert on peopleOrder
for each row
declare
dis_num integer;
begin
select count(*)
into dis_num
from product
where discounted != 'N'
and product_name = :new.product_name
and rownum < 2;
if dis_num != 0 then
raise_application_error(-20001, 'No discounted product can be ordered!');
end if;
end constraint_1;
-- Orig ==============================================
CREATE OR REPLACE TRIGGER constraint_1
before UPDATE OR INSERT on peopleOrder
for each row
begin
if :new.discounted = 'N' then
RAISE_APPLICATION_ERROR(-20001, 'No discounted product can be ordered')
end if;
END;
/

Related

PL/SQL - Simple trigger, take from one column and IF-THEN-ELSE to another column

I am trying to figure out where I am going wrong on this simple trigger. I am quite new to triggers and trying to get used to using them with IFTTT statements.
I want the trigger to watch for a new row entry, and if the value is within a certain range within a column (col_a) it will then enter a certain value in the same row, but different column (col_b), which will be NULL up until this is entered. Please can you help?
CREATE TRIGGER trg_test
BEFORE INSERT
ON test_table
FOR EACH ROW
BEGIN
IF :new.col_a >= 10
THEN :new.col_b := 'High';
ELSE
:new.col_b := 'Low';
END IF;
END;
It just keeps coming back with "success with compilation error".
You can easily do this using a virtual column:
alter table test_table
add col_b generated always as (case when col_a >= 10 then 'High' else 'Low' end);
I wish that exercises in triggers used reasonable use-cases. This is not one -- you need both an insert and an update trigger, for instance.

how to trigger an update on a table row value using a row value from another table in Apex Oracle SQL?

i have two tables; Purchase order:
CREATE TABLE "PURCHASE_ORDER"
( "PO_NUMBER" NUMBER(*,0) NOT NULL ENABLE,
"CUSTOMER_NUMBER" NUMBER(*,0),
"PO_DATE" DATE,
"PRICE" NUMBER(*,0),
"ORDER_QUANTITY" NUMBER,
"STOCK_ID" NUMBER(*,0),
PRIMARY KEY ("PO_NUMBER")
and the bulk stock table:
CREATE TABLE "BULK_STOCK"
( "STOCK_ID" NUMBER(*,0) NOT NULL ENABLE,
"STOCK_DESCRIPTION" VARCHAR2(50),
"STOCK_UNITOF_MEASUREMENT" VARCHAR2(50),
"STOCK_STATUS" VARCHAR2(50),
"FLOOR_ID" NUMBER(*,0),
"STOCK_NAME" VARCHAR2(50),
"BULK_QUANTITY" NUMBER NOT NULL ENABLE,
PRIMARY KEY ("STOCK_ID")
I am creating a trigger that updates the BULK_QUANTITY in the bulk stock table when purchase table is inserted with values:
create or replace trigger "UPDATE_ON_PURCHASE"
BEFORE
insert or update or delete on "PURCHASE_ORDER"
for each row
begin
UPDATE bulk_stock
SET BULK_QUANTITY =BULK_QUANTITY-:old.ORDER_QUANTITY
WHERE STOCK_ID=:old.STOCK_ID;
end;
when i run this form
Nothing changes in the bulk stock table
but if I hard code it
create or replace trigger "UPDATE_ON_PURCHASE"
BEFORE
insert or update or delete on "PURCHASE_ORDER"
for each row
begin
UPDATE bulk_stock
SET BULK_QUANTITY =BULK_QUANTITY- 20 //the 20 would be the inserted ORDER_QUANTITY
WHERE STOCK_ID= 12 // 12 would be the stock_ID
it works.
This is done in Oracle Apex software. I need help with the trigger. Many thanks
end;
You're using the :OLD values in your trigger which seems problematic. On an INSERT the :OLD values are all NULL. So in the case of an INSERT, at the very least, it seems you'd want to use the :NEW values.
On an UPDATE the :OLD values contain the pre-update values. I don't know how your stock table is being maintained but it seems to me that you'd want to add the :OLD values back into stock then remove the :NEW values from stock, assuming that both the ORDER_QUANTITY and STOCK_ID can change.
When you're doing a DELETE the :OLD values contain the pre-deletion values, but the :NEW values are all NULL (makes sense, if you think about it). So in the case of a deletion it would seem that you'd want to use the :OLD values. However, if you're deleting a PO do you really want to adjust the stock? I'd think you'd need some type of status on the order to let you know if it's been fulfilled or cancelled or whatever, and only add the stock back into the bulk stock table if the order was never fulfilled.
In any case, one way to re-write your trigger would be:
create or replace trigger UPDATE_ON_PURCHASE
BEFORE insert or update or delete on PURCHASE_ORDER
for each row
begin
IF INSERTING THEN
UPDATE bulk_stock
SET BULK_QUANTITY = BULK_QUANTITY - :NEW.ORDER_QUANTITY
WHERE STOCK_ID = :NEW.STOCK_ID;
ELSIF UPDATING THEN
UPDATE BULK_STOCK
SET BULK_QUANTITY = BULK_QUANTITY + :OLD.ORDER_QUANTITY
WHERE STOCK_ID = :OLD.STOCK_ID;
UPDATE BULK_STOCK
SET BULK_QUANTITY = BULK_QUANTITY - :NEW.ORDER_QUANTITY
WHERE STOCK_ID = :NEW.STOCK_ID;
ELSIF DELETING THEN
UPDATE BULK_STOCK
SET BULK_QUANTITY = BULK_QUANTITY + :OLD.ORDER_QUANTITY
WHERE STOCK_ID = :OLD.STOCK_ID;
END IF;
end;
I'm not sure that this logic is really what you wanted as I don't understand completely what you're trying to do, particularly in the DELETE case, so take it as an example and apply whatever logic your situation calls for.
I'll also say that I think putting this logic in a trigger is a bad choice. Business logic such as this should not be implemented in a trigger - better to put it into a procedure and call the procedure when needed. Putting business logic into triggers can be problematic.
Best of luck.

History table referencing other values in the table / accessing package table variables

I have a system for tracking usage of computers in a lab. Slightly simplified, it works out to:
Machines are associated with a lab.
Machines have a binary logged_in state, which gets updated automatically when users log in and out.
There is a view keyed on the lab which gathers the total number of seats associated with the lab, and the current number in use for that lab.
What I would like to do is add a history or audit table, which would track changes to lab population over time. I had a trigger on the machine table to store the time and the total lab population in my lab history table every time the machine table changed. The problem is that, in order to get the new total for the lab, I have to examine the other values in the machine table. This results in a table mutating error.
Some things I found on here and elsewhere suggested that I should create a package to track the labs being changed. Use a before trigger to clear the list, a row trigger to store each labid being changed, and an after trigger to update the history table with new values for only those labs whose ids are in the list. I've tried that, but can't figure out how to access the values I've stored in the package table (or even if it is storing them properly in the first place.) As will no doubt be obvious, I'm unfamiliar with PL/SQL packages and table variables - the whole syntax of referring to table entries like arrays struck me as vaguely heretical though incredibly useful if it works. So most of the below is just copied and adapted from other solutions I've found, but they didn't stretch as far as how to actually use my table of changed lablocids, assuming its being created properly in the first place. The following simply tells me that pg_machine_in_use_pkg.changedlablocids does not exist when I try to compile the final trigger.
create or replace package labstats_adm.pg_machine_in_use_pkg
as
type arr is table of number index by binary_integer;
changedlablocids arr;
empty arr;
end;
/
create or replace trigger labstats_adm.pg_machine_in_use_init
before insert or update
on labstats_adm.pg_machine
begin
-- begin each update with a blank list of changed lablocids
pg_machine_in_use_pkg.changedlablocids := pg_machine_in_use_pkg.empty;
end;
/
--
create or replace trigger labstats_adm.pg_machine_in_use_update
after insert or update of in_use,lablocid
on labstats_adm.pg_machine
for each row
begin
-- record lablocids - old and new - of changed machines
if :new.lablocid is not null then
pg_machine_in_use_pkg.changedlablocids( pg_machine_in_use_pkg.changedlablocids.count+1 ) := :new.lablocid;
end if;
if :old.lablocid is not null and :old.lablocid != :new.lablocid then
pg_machine_in_use_pkg.changedlablocids( pg_machine_in_use_pkg.changedlablocids.count+1 ) := :old.lablocid;
end if;
end;
create or replace trigger labstats_adm.pg_machine_lab_history
after insert or update of in_use,lablocid
on labstats_adm.pg_machine
begin
-- for each lablocation we just logged a change to, update that labs history
insert into labstats_adm.pg_lab_history (labid, time, total_seats, used_seats)
select labid, systimestamp, total_seats, used_seats
from labstats_adm.lab_usage
where labid in (
select distinct labid from pg_machine_in_use_pkg.changedlablocids
);
end;
/
On the other hand, if there is a better overall approach than the package, I'm all ears.
After some reflection I've got to go with #tbone on this one. In my experience a history table should be a copy of the data in the "real" table with fields added to show when particular 'version' of the data shown by a row in the history table was in effect. So if the "real" table is something like
CREATE TABLE REAL_TABLE
(ID_REAL_TABLE NUMBER PRIMARY KEY,
COL2 NUMBER,
COL3 VARCHAR2(50));
then I'd create the history table as
CREATE TABLE HIST_TABLE
(ID_HIST_TABLE NUMBER PRIMARY KEY
ID_REAL_TABLE NUMBER,
COL2 NUMBER,
COL3 VARCHAR2(50),
EFFECTIVE_START_DT TIMESTAMP(9) NOT NULL,
EFFECTIVE_END_DT TIMESTAMP(9));
and I'd have the following triggers to get everything populated:
CREATE TRIGGER REAL_TABLE_BI
BEFORE INSERT ON REAL_TABLE
REFERENCING OLD AS OLD
NEW AS NEW
FOR EACH ROW
BEGIN
IF :NEW.ID_REAL_TABLE IS NULL THEN
:NEW.ID_REAL_TABLE := REAL_TABLE_SEQUENCE.NEXTVAL;
END IF;
END REAL_TABLE_BI;
CREATE TRIGGER HIST_TABLE_BI
BEFORE INSERT ON HIST_TABLE
FOR EACH ROW
BEGIN
IF :NEW.ID_HIST_TABLE IS NULL THEN
:NEW.ID_HIST_TABLE := HIST_TABLE_SEQUENCE.NEXTVAL;
END IF;
END HIST_TABLE_BI;
CREATE TRIGGER REAL_TABLE_AIUD
AFTER INSERT OR UPDATE OR DELETE ON REAL_TABLE
FOR EACH ROW
DECLARE
tsEffective_start_date TIMESTAMP(9) := SYSTIMESTAMP;
tsEffective_end_date TIMESTAMP(9) := dtEffective_start_date - INTERVAL '0.000000001' SECOND;
BEGIN
IF UPDATING OR DELETING THEN
UPDATE HIST_TABLE
SET EFFECTIVE_END_DATE := tsEffective_end_date
WHERE ID_REAL_TABLE = :NEW.ID_REAL_TABLE AND
EFFECTIVE_END_DATE IS NULL;
END IF;
IF INSERTING OR UPDATING THEN
INSERT INTO HIST_TABLE (ID_REAL_TABLE, COL2, COL3, EFFECTIVE_START_DATE)
VALUES (:NEW.ID_REAL_TABLE, :NEW.COL2, :NEW.COL3, tsEffective_start_date);
END IF;
END REAL_TABLE_AIUD;
Using this method the "history" table has all historical versions of the data in the "real" table PLUS a complete copy of the "current" data from the "real" table; this is done to simplify queries which need to report on all versions of the data in the table up to and including present values.
The advantage of using triggers to do all this is that the maintenance of the primary keys and the history table becomes automatic and can't be easily circumvented or forgotten.
Share and enjoy.
Sorry so slow to get back; its taken me a bit of fiddling, and I haven't had a lot of time to work on it.
Thanks to Bob Jarvis for pointing me at the compound triggers, which cleaned up the overall structure significantly. After that, I just had to sanitise the way I'm getting values back out of my table variable. On the odd chance that someone else stumbles over this looking for the answer to the same problem, I'll post my final solution here:
create or replace
trigger pg_machine_in_use_update
for insert or update or delete of in_use,lablocid
on labstats_adm.pg_machine
compound trigger
type arr is table of number index by binary_integer;
changedlabids arr;
idx binary_integer;
after each row is
newlabid labstats_adm.pg_labs.labid%TYPE;
oldlabid labstats_adm.pg_labs.labid%TYPE;
begin
-- store the labids of any changed locations
-- PL/SQL does not like us testing for the existence of something that isn't there, so just set it twice if necessary
if ( :new.lablocid is not null ) then
select labid into newlabid from labstats_adm.pg_lablocation where lablocid = :new.lablocid;
changedlabids( newlabid ) := 1;
end if;
if ( :old.lablocid is not null ) then
select labid into oldlabid from labstats_adm.pg_lablocation where lablocid = :old.lablocid;
changedlabids( oldlabid ) := 1;
end if;
end after each row;
after statement is
begin
idx := changedlabids.FIRST;
while idx is not null loop
insert into labstats_adm.pg_lab_history (labid, time, total_seats, used_seats)
select labid, systimestamp, total_seats, used_seats
from labstats_adm.lab_usage
where labid = idx;
idx := changedlabids.NEXT(idx);
end loop;
end after statement;
end pg_machine_in_use_update;

PL/SQL Conditional statement using two tables within trigger

I'm working on a simple trigger that updates the Quantity on Hand of a product when adding a new invoice line.
I got that working fine; but I starting thinking that in a practical application it would be prudent to verify that the QOH is sufficient before allowing the update. I'm sure I could write a separate trigger for that, but I'd like to see if it's possible to join two tables for a conditional statement within a trigger.
This is as far as I've gotten; adding a SELECT statement anywhere in there causes all hell to break loose, so I'm a little stumped how I can declare the PRODUCT.P_QOH before calling the conditional.
CREATE OR REPLACE TRIGGER trg_prod_QOH_on_line_add
BEFORE INSERT ON LINE
FOR EACH ROW
BEGIN
IF :NEW.LINE_UNITS > PRODUCT.P_QOH THEN
RAISE_APPLICATION_ERROR(-20202, 'Insufficient quantity on hand');
ELSE
UPDATE PRODUCT
SET P_QOH = P+QOH - :NEW.LINE_UNITS;
WHERE PRODUC.P_CODE = :NEW.P_CODE;
END IF;
END;
/
This isn't a major problem for me, as I said there is probably a different way; I'm just starting to learn this stuff and would like to see what's possible. Thanks for your help.
You get into dangerous territory trying to enforce rules like this via triggers. The solution you are asking for is:
create or replace trigger trg_prod_qoh_on_line_add
before insert on line
for each row
declare
v_qoh product.p_qoh%type;
begin
select p_qoh
into v_qoh
from product
where product.p_code = :new.p_code;
if :new.line_units > v_qoh then
raise_application_error(-20202, 'Insufficient quantity on hand');
else
update product
set p_qoh = p_qoh - :new.line_units
where product.p_code = :new.p_code;
end if;
end;
However this is not a safe solution in a system with more than one concurrent user. Suppose product 'X' has p_qoh=10 and then 2 users do this:
user1> insert into line (p_code, line_units) values ('X', 7);
user2> insert into line (p_code, line_units) values ('X', 8);
user1> commit;
user2> commit;
Both sessions will see that 'X' has p_qoh = 10 so both will succeed and product.p_qoh will end up as -5. All is corrupt!
The safe solution would be to create a check constraint on product:
alter table product add constraint prod_qoh_chk check (p_qoh >= 0);
Now your trigger can be simply:
create or replace trigger trg_prod_qoh_on_line_add
before insert on line
for each row
begin
update product
set p_qoh = p+qoh - :new.line_units;
where produc.p_code = :new.p_code;
end;
This would raise a less friendly error message like:
ORA-02290: check constraint (MYSCHEMA.PROD_QOH_CHECK) violated
You can trap this in your trigger and give the message you want:
create or replace trigger trg_prod_qoh_on_line_add
before insert on line
for each row
begin
update product
set p_qoh = p+qoh - :new.line_units;
where produc.p_code = :new.p_code;
exception
when others then
if sqlerrm like 'ORA-02291:%(MYSCHEMA.PROD_QOH_CHECK)%' then
raise_application_error(-20202,'Insufficient quantity on hand');
else
raise;
end if;
end;
Now if we re-run the 2 user scenario above:
user1> insert into line (p_code, line_units) values ('X', 7);
user2> insert into line (p_code, line_units) values ('X', 8);
user1> commit;
At this point user2's insert fails with the error message:
ORA-20202: Insufficient quantity on hand

How to insert a record into multiple tables using a trigger?

I have two Tables.
I want to insert the same record on both tables at the same time.
I.e., while I insert a record for the first table, this same record also is inserted in the second table using a trigger.
Do you have any experience/advice in this process ?
if you're using stored procedures you can easily manage this
CREATE PROCEDURE sp_Insert
#Value varchar(10)
AS
insert into table1 (...,...) values (#value,...)
insert into table2 (...,...) values (#value,...)
I would suggest using Erik's method over a trigger. Triggers tend to cause performance issues, and a lot of times, you forget that the trigger exists, and get unexpected behavior. If you do want to use a trigger however, it will work. here is an example:
CREATE TRIGGER trgTest ON Test
FOR INSERT
AS
INSERT Test2
(Id, value)
SELECT Id, Value
FROM Inserted
Can Use Cursor Concept!
CREATE OR REPLACE TRIGGER bi_order
BEFORE INSERT
ON ord
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
WHEN (NEW.payment_type = 'CREDIT')
DECLARE
CURSOR cur_check_customer IS
SELECT 'x'
FROM customer
WHERE customer_id = :NEW.customer_id
AND credit_rating = 'POOR';
lv_temp_txt VARCHAR2(1);
lv_poor_credit_excep EXCEPTION;
BEGIN
OPEN cur_check_customer;
FETCH cur_check_customer INTO lv_temp_txt;
IF (cur_check_customer%FOUND) THEN
CLOSE cur_check_customer;
RAISE lv_poor_credit_excep;
ELSE
CLOSE cur_check_customer;
END IF;
EXCEPTION
WHEN lv_poor_credit_excep THEN
RAISE_APPLICATION_ERROR(-20111, 'Cannot process CREDIT ' ||
'order for a customer with a POOR credit rating.');
WHEN OTHERS THEN
RAISE_APPLICATION_ERROR(-20122, 'Unhandled error occurred in' ||
' BI_ORDER trigger for order#:' || TO_CHAR(:NEW.ORDER_ID));
END bi_order;