How to write an instead of update trigger correctly? - sql

Imagine the following example with the table t and the view v.
create table t (a int, b int, c int);
insert into t values (1, 2, 3);
create view v as select * from t;
Now I want the write an "instead of update" trigger for the view in that way, that all updates of the view will update the table. I know that I do not need it in this simplified example, because PostgreSQL can do it automatically. But in my real world use case it is necessary.
Is this the correct way to implement the update trigger?
create function u () returns trigger as $$
begin
update t set a = new.a, b = new.b, c = new.c;
return new;
end;
$$ language plpgsql;
create trigger t instead of update on v
for each row execute procedure u ();
I am not sure, because I am wondering whether there is a difference between the update of the table and the view:
update t set a = 0 where a = 1;
update v set a = -1 where a = 0;
I expect that the update of the table updates only one column. But I fear that the update of the view updates three columns in the table.
Is this the case? And if so how to work around this?

Any update on a table, no matter how many rows it modifies, will always write the same amount of data. The reason is that an UPDATE creates a new version of the complete row.
The UPDATE on the view will take longer, because calling a trigger is some overhead.
By the way, you don't need that trigger at all. Simple views like that are automatically updateable, even without a trigger.

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.

UPSERT inserts duplicate null entry into table (ORACLE)

I am trying to make an upsert trigger on ORACLE via PL/SQL by checking some examples, i am doing fine, i think it is the last step i should only configure. My requirement is that :
A system that will insert to that field will remain one column always null, so i will read column value from another table, then upsert it with inclusion of that value.
d2c_region_locale_config holds d2c_is_active value, so i firstly read that value regarding to locale condition then trigger inserts or updates to table with addition of this value on active_for_d2c column.(for update i am using locale and country columns as it is shown on where clause, they are not PK but has not null condition)
So i've created this trigger:
CREATE OR REPLACE TRIGGER BL_PIM_LOCALE_COUNTRY
BEFORE INSERT OR UPDATE ON PIM_LOCALE_COUNTRY REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
DECLARE
l_active_for_d2c INTEGER;
BEGIN
if :NEW.active_for_d2c is null then
DELETE from pim_locale_country where active_for_d2c is null;
select distinct(d2c_isactive) into l_active_for_d2c from d2c_region_locale_config where d2c_locale= :NEW.locale;
UPDATE pim_locale_country
SET locale = :NEW.locale, locale_name = :NEW.locale_name,
country = :NEW.country, country_name = :NEW.country_name, isdummy = :NEW.isdummy,
active_for_d2c = l_active_for_d2c, itextpos = :NEW.itextpos, locale_charset = :NEW.locale_charset,
fallback_locale = :NEW.fallback_locale, default_for_lang = :NEW.default_for_lang, opeclang = :NEW.opeclang
where locale = :NEW.locale and country = :NEW.country;
IF ( sql%notfound ) THEN
INSERT INTO PIM_LOCALE_COUNTRY (locale,locale_name,country,country_name,isdummy,active_for_d2c,itextpos,locale_charset,fallback_locale,default_for_lang,opeclang)
VALUES (:NEW.locale, :NEW.locale_name,:NEW.country,:NEW.country_name,:NEW.isdummy,l_active_for_d2c,:NEW.itextpos,:NEW.locale_charset,:NEW.fallback_locale,:NEW.default_for_lang,:NEW.opeclang);
END IF;
end if;
END;
It currently does the job, reads value and inserts or updates the existing locale-country couple for other values. But critical thing is that, table always has one "null" value(Please check screenshot), even that i run delete statement at the beginning on my trigger. So my question would be how to delete, or how to make this approach on trigger side ?
Many thanks for answers!
Trigger before insert doesn't block insert itself, so you insert that record twice. That is, once your trigger done its work (inserted or updated record), oracle will proceed with insert (or update) using values that stand in NEW record of your trigger. If trigger modifies NEW., it will be stored as you changed it, but if trigger inserts something itself, you can get more records.
You can use instead of insert or update triggers, and then oracle will not run its own inserts/updates after trigger finishes.
But more common way for 1-record triggers is to modify fields in NEW, for this case field NEW.d2c_is_active.
It looks like this (possible typos, please check)
CREATE OR REPLACE TRIGGER BL_PIM_LOCALE_COUNTRY
BEFORE INSERT OR UPDATE ON PIM_LOCALE_COUNTRY REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
BEGIN
if :NEW.active_for_d2c is null then
select d2c_isactive
into :NEW.active_for_d2c
from d2c_region_locale_config
where d2c_locale= :NEW.locale and rownum<=1;
end if;
END;

Instead of triggers in PostgreSQL, how can I stop repeating column names?

I am trying to create a simple table inheritance hierarchy from my entity relationship model in PostgreSQL.
For this, I have created the following tables:
CREATE TABLE base (
id integer PRIMARY KEY,
title varchar(40),
test integer
);
CREATE TABLE sub (
id integer REFERENCES base(id) PRIMARY KEY,
count integer
);
Now, since how the inheritance is solved on the DB does not need to concern the application, I also created a view that the application will access. All operations from the application should be performed on the view, and as such, I also need an instead of trigger to perform updates, inserts and deletes. The view definition and the trigger looks like this:
CREATE VIEW sub_view AS
SELECT s.count, b.title, b.test, b.id FROM sub s
JOIN base b ON b.id = s.id;
--TRIGGER
CREATE OR REPLACE FUNCTION instead_of_f()
RETURNS trigger AS
$$
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO base(id, title, test) VALUES (NEW.id, NEW.title, NEW.test);
INSERT INTO sub(id, test) VALUES (NEW.id, NEW.test);
RETURN NEW;
ELSIF TG_OP = 'UPDATE' THEN
UPDATE base SET title = NEW.title, test = NEW.test WHERE id = OLD.id;
UPDATE sub SET count = NEW.count WHERE id = OLD.id;
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
DELETE FROM sub WHERE id = OLD.id;
DELETE FROM base WHERE id = OLD.id;
RETURN NULL;
END IF;
END;
$$ LANGUAGE PLPGSQL;
CREATE TRIGGER instead_of_dml_trig
INSTEAD OF INSERT OR UPDATE OR DELETE ON
sub_view FOR EACH ROW
EXECUTE PROCEDURE instead_of_f();
This basically works fine, but it is tedious and not very maintainable to have to repeat all the column names over and over again. Ideally, I would like to write something like this for the view instead:
CREATE VIEW sub_view AS
SELECT * FROM sub s JOIN base b ON b.id = s.id;
And instead of the insert statements in the trigger:
INSERT INTO base VALUES NEW.*;
INSERT INTO sub VALUES NEW.*;
Is this somehow possible? I couldn't find anything similar, except for audit triggers, and those just saved the NEW and OLD records as a string. In this contrived example it would be easy enough to add new columns or delete them as the base/sub tables change, but as soon as there are a few more sub tables and more columns this becomes practically unmaintainable.
Like Craig suggested, I ended up solving it in my application directly, not on the database. My application knows all the relevant columns from the views, as well as the base types, so it ended up being easier to just create the trigger and the view in the migrations there.

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;