Using a table to store deleted rows - sql

In my Oracle DB I've a table called tableA where an application write data. My program reads from tableA and when I've processed the data I delete them. The question is I want to keep a log of every data I've processed and I can't keep them in tableA because I've no control over application A and it might not work if I keep my processed data on that table, so I've created a table identical to tableA called tableB and I've put this trigger on tableA:
create or replace
trigger tableA_delete_trigger
BEFORE DELETE ON tableA
FOR EACH ROW
BEGIN
INSERT INTO tableB
( a,
b,
c,
)
VALUES
( :old.a,
:old.b,
:old.c,
sysdate);
END;
This system work quite well, the real problem is when I need to alter something in tableA I have to replicate by hand the same modification in tableB and if I add/remove coloumn I have to update the trigger.
Is there a better way to do this?

An alternative approach might be to rename TableA and create a view named TableA for application A to use. You would then logically delete rows by whatever means seem appropriate and only expose in the view the rows that are not deleted.
You would still need to modify the view if the table structure changes, but at least you won't need to worry about the trigger.

What about an alter trigger:
CREATE OR REPLACE TRIGGER ddl_trigger AFTER ALTER ON schema
DECLARE
cmd VARCHAR2(32000);
BEGIN
SELECT
upper(sql_text)
INTO
cmd
FROM
v$open_cursor
WHERE
upper(sql_text) LIKE 'ALTER TABLE TABLEA%' ;
SELECT
REPLACE(cmd, 'TABLEA', 'TABLEB')
INTO
cmd
FROM
dual;
EXECUTE IMMEDIATE cmd;
END;
Not sure that will work because of the recursion.

Related

How to write a generic update trigger for PostgreSQL view without known columns?

I'm using a PostgreSQL view to simulate a legacy table that has since been split into two separate tables, so we can maintain backwards compatibility with a range of services. The goal is essentially for this view to function transparently as if it was the original table before the split. Using an INSTEAD OF INSERT trigger I've been able to handle inserts into the two tables pretty easily.
However I'm struggling to figure out how to write an INSTEAD OF UPDATE trigger, because I don't know which columns will be changed in any one request (I'm using sequelize so I don't really have any way of controlling what the requests are going to be, it could be SET a single column, it could be all of them). All examples I've found online seem to be updating known fields (either some meta-column related to the query or knowing what the shape of the update is going to be).
CREATE FUNCTION update_legacy_table_trigger()
RETURNS trigger AS $$
BEGIN
UPDATE table_a SET ??? WHERE id = NEW.id;
UPDATE table_b SET ??? WHERE table_a_id = NEW.id;
END;
$$ LANGUAGE plpgsql;
A few example cases:
Imagine:
legacy_table is the name of the view
table_a has the columns: id, name, description, type, shared_column and
table_b has the columns: id, table_a_id (foreign key to table_a), price, shared_column.
Example A:
UPDATE legacy_table SET name="Test", price="10.00" WHERE id="123";
I'd expect the trigger to behave something like:
UPDATE table_a SET name="Test" WHERE id="123";
UPDATE table_b SET price="10.00" WHERE table_a_id="123";
Example B: it could also receive updates for only one table
UPDATE legacy_table SET price="10.00" WHERE id="123";
So we'd expect the trigger to behave like:
UPDATE table_b SET price="10.00" WHERE table_a_id="123";
Example C: perhaps a lot of columns in the one request
UPDATE legacy_table SET name="Test", price="10.00", description="A test", type="foo", shared_column="bar" WHERE id="123";
So the trigger should behave like:
UPDATE table_a SET name="Test", description="A test", type="foo", shared_column="bar" WHERE id="123";
UPDATE table_b SET price="10.00", shared_column="bar" WHERE table_a_id="123";
There might be cases where updates need to be made to both tables, but I don't believe there are any cases where the name of the column in the original UPDATE would be any different from the name of the columns in the new tables.
How would I go about writing this INSTEAD OF UPDATE trigger for these cases where I don't know explicitly what the UPDATE query will be?
You have to write the update statement for all the columns in your trigger function. Your trigger function will look like below:
CREATE FUNCTION update_legacy_table_trigger()
RETURNS trigger AS $$
BEGIN
UPDATE table_a SET name=new.name, description=new.description,type=new.type, shared_column=new.shared_column WHERE id = NEW.id;
UPDATE table_b SET price=new.price, shared_columns=new.shared_columns WHERE table_a_id = NEW.id;
return null;
END;
$$
LANGUAGE plpgsql;
and you have to write your trigger on your view as below:
CREATE TRIGGER trg_update_legacy
INSTEAD OF UPDATE
ON legacy_table
FOR EACH ROW
EXECUTE PROCEDURE update_legacy_table_trigger();
DEMO
This will handle all the update cases because NEW will contain updated row and we are updating all the fields with the data of NEW row.

TRIGGER ON VIEW in PostgreSQL doesn't trigger

I'm trying to add a trigger on a VIEW in PostgreSQL 9.6.
This is my view:
CREATE VIEW names AS
SELECT one.name AS name_one, two.name AS name_two, three.name AS name_three
FROM table_one one
LEFT JOIN table_two two ON one.id = two.id
LEFT JOIN table_three three ON two.id = three.id;
This is my trigger function:
CREATE OR REPLACE FUNCTION notify_name_changed() RETURNS trigger AS $BODY$
BEGIN
PERFORM pg_notify('name_changed', row_to_json(NEW)::text);
RETURN NULL;
END;
$BODY$ LANGUAGE plpgsql;
And my CREATE TRIGGER:
CREATE TRIGGER notify_name_changed INSTEAD OF INSERT OR UPDATE OR DELETE ON "names"
FOR EACH ROW EXECUTE PROCEDURE notify_name_changed();
This doesn't fire any changes whenever something happens in one of the base tables.
However, creating 3 individual triggers does, but is somewhat unrelated to the view:
CREATE TRIGGER notify_name_changed AFTER INSERT OR UPDATE OR DELETE ON "one"
FOR EACH ROW EXECUTE PROCEDURE notify_name_changed();
CREATE TRIGGER notify_name_changed AFTER INSERT OR UPDATE OR DELETE ON "two"
FOR EACH ROW EXECUTE PROCEDURE notify_name_changed();
CREATE TRIGGER notify_name_changed AFTER INSERT OR UPDATE OR DELETE ON "three"
FOR EACH ROW EXECUTE PROCEDURE notify_name_changed();
Isn't it possible to add a trigger directly on a view, which fires in the event of any changes in base tables used in that view?
I think you misunderstand the concept of a view.
A view does not hold any data, you can see it as a “crystallized SQL statement” that has a name. Whenever a view is used in a query, it is replaced by its definition in the “query rewrite” step.
An INSTEAD OF trigger for UPDATE on a view is triggered only if you update the view itself, not the underlying tables. For that, you'd have to define triggers on those tables.
The point that you are probably missing is that if something changes in the underlying tables, it is immediately changed in the view, since the view is just a query on the base table.

Oracle trigger- instead of delete, update the row

How do I write an Oracle trigger, than when a user deletes a certain record, the delete doesnt actually happen, but instead performs an update on those rows and sets the status of the record to 'D'?
I tried:
create or replace
trigger DELFOUR.T4M_ITEM_ONDELETE
before delete on M_ITEM_H
FOR EACH ROW
BEGIN
UPDATE
M_ITEM_H
SET
ITEM_STAT = 'D'
WHERE
CUST_CODE = 'TEST'
AND ITEM_CODE = 'GDAY'
;
raise_application_error(-20000,'Cannot delete item');
END;
But I am getting mutating table errors. Is this possible?
If you really need a trigger, the more logical approach would be to create a view, create an INSEAD OF DELETE trigger on the view, and to force the applications to issue their deletes against the view rather than against the base table.
CREATE VIEW vw_m_item_h
AS
SELECT *
FROM m_item_h;
CREATE OR REPLACE TRIGGER t4m_item_ondelete
INSTEAD OF DELETE ON vw_m_item_h
FOR EACH ROW
AS
BEGIN
UPDATE m_item_h
SET item_stat = 'D'
WHERE <<primary key>> = :old.<<primary key>>;
END;
Better yet, you would dispense with the trigger, create a delete_item procedure that your application would call rather than issuing a DELETE and that procedure would simply update the row to set the item_stat column rather than deleting the row.
If you really, really, really want a solution that involves a trigger on the table itself, you could
Create a package with a member that is a collection of records that map to the data in the m_item_h table
Create a before delete statement-level trigger that empties this collection
Create a before delete row-level trigger that inserts the :old.<<primary key>> and all the other :old values into the collection
Create an after delete statement-level trigger that iterates through the collection, re-inserts the rows into the table, and sets the item_stat column.
This would involve more work than an instead of trigger since you'd have to delete and then re-insert the row and it would involve way more moving pieces so it would be much less elegant. But it would work.
First of all the trigger you wrote would throw a mutating table error. Technically what you are asking is not possible i.e. delete wouldn't delete but rather update, unless you raise an exception in the middle which could be an ugly way of doing it. I would think users using some sort of application front end which lets them delete data using a delete button, so you may use an update statement there instead of a delete statement.
Another option would be to create a log table, where you could insert the record before deleting it from the actual table and then join the log table with the actual table to retrieve deleted records. Something like-
CRETAE TABLE M_ITEM_H_DEL_LOG as SELECT * FROM M_ITEM_H WHERE 1=2;
And then
create or replace
trigger DELFOUR.T4M_ITEM_ONDELETE
before delete on M_ITEM_H
FOR EACH ROW
BEGIN
INSERT INTO
M_ITEM_H_DEL_LOG
VALUES (:old.col1, :old.col2,.....) --col1, col2...are columns in M_ITEM_H
;
END;

Trigger to update another field in same table

I want to run a trigger when I update a certain field on the database, so it updates another field (basically we have 2 different unique IDs for each record, so when one is changed, we need to update the other too - yay!)
CREATE OR REPLACE TRIGGER trigger_name ON table AFTER
UPDATE AS
UPDATE table A
SET unique_to_update = NVL(
(SELECT b.unique_to_update_from
FROM table b
WHERE B.other_unique_id = A.unique_id_to_match
), 0);
I have no idea if this works (scared to test it, quite frankly, since I'm certain it'll break things) and even if it did, it'd run on every single update of that table - not just the one field that I wanted.
Any help would be much appreciated, thank you!
Test anything before putting it in production.
Something like this shoud be your trigger:
CREATE OR REPLACE TRIGGER trigger_name
BEFORE UPDATE of unique_id_to_match
ON table
FOR EACH ROW
AS
BEGIN
select
NVL(
(SELECT b.unique_to_update_from
FROM table b
WHERE B.other_unique_id = :new.unique_id_to_match
), 0)
into :new.unique_to_update
FROM dual;
END;
5000 rows x 900 columns is not so big :)
I was afraid it has 10M of rows :)
OK start
create temporary table tmp_my_important_columns on commit delete rows
as
select unique_id_to_match, unique_to_update_from,
other_unique_id , unique_to_update
from table where rownum < 1;
second
CREATE OR REPLACE TRIGGER trigger_before_upd_stmt
BEFORE UPDATE
ON table
AS
BEGIN
insert into
tmp_my_important_columns
select unique_id_to_match, unique_to_update_from,
other_unique_id , unique_to_update
from table;
END;
third,
CREATE OR REPLACE TRIGGER trigger_name
BEFORE UPDATE of unique_id_to_match
ON table
FOR EACH ROW
AS
BEGIN
select
NVL(
(SELECT b.unique_to_update_from
FROM tmp_my_important_columns b
WHERE B.other_unique_id = :new.unique_id_to_match
), 0)
into :new.unique_to_update
FROM dual;
END;
Comments:
After an update, if you update the same rows again, without commit
(which deletes the tmp table), you'll get problems. So, or you commit
after update, or you can add an after update trigger(without for each row) that deletes all, from tmp table, but this is somehow ugly.
You can add indexes on the temporary table.

Is it possible to move a record from one table to another using a single SQL statement?

I need a query to move a record from one table to another without using multiple statements?
No, you cannot move records in one SQL statement. You have to use an INSERT followed by a DELETE statement. You should wrap these statements into a transaction, to make sure that the copy operation remains atomic.
START TRANSACTION;
INSERT INTO
new_table
SELECT
*
FROM
old_table
WHERE
some_field = 'your_criteria';
DELETE FROM old_table WHERE some_field = 'your_criteria';
COMMIT;
If you really want to do this in a single SQL statement, one way to accomplish this would be to create an "after delete" trigger on the source table that inserts the row into the target table. This way you can move the row from the source table to the target table simply by deleting it from the source table. Of course this will only work if you want to insert into target table for every delete on the source table.
DELIMITER $$
DROP TRIGGER IF EXISTS TR_A_DEL_SOURCE_TABLE $$
CREATE TRIGGER TR_A_DEL_SOURCE_TABLE AFTER DELETE ON SOURCE_TABLE FOR EACH ROW BEGIN
INSERT IGNORE INTO TARGET_TABLE(id,val1,val2) VALUES(old.id,old.va1,old.val2);
END $$
DELIMITER ;
So to move the row with id 42 from source table to target table:
delete from source_table where id = 42;
No - you might be able to do the INSERT in one nested statement, but you still need to remove the record.
There is no way to make it a single query, but if you HAVE to do it in a single query within an application you can create a Stored Procedure to do it for you.
DELIMITER $$
DROP PROCEDURE IF EXISTS `copydelete` $$
CREATE PROCEDURE `copydelete` (id INT)
BEGIN
INSERT INTO New_Table SELECT * from Old_Table WHERE Old_Table.IdField=id;
DELETE FROM Old_Table where IdField=id;
END $$
DELIMITER ;
Then you're new query is just
CALL copydelete(4);
Which will delete WHERE IdField=4;
Please note that the time delay between insert-select and delete can cause you to delete to much.
For a safe route you could use an update field:
update old_table set move_flag=1 where your_criteria
insert into ... select from ... where move_flag = 1
delete from old_table where move_flag=1
Or use a transaction which locks the old_table so no data can be added between insert... select and delete.