I made an after update trigger which I intended to use as an audit log for changes to the primary record in source table. Everything is working as expected except when I view my trigger's definition I can see it resolved all columns for MYLIB.NMF00PAUDI and OTBL which means that I have to recreate this trigger for every column I add into source and audit tables.
Can I somehow retain SELECT *? This way I only need to modfiy source and audit tables.
CREATE TRIGGER MYLIB.NMF00PAUDIT
AFTER UPDATE ON MYLIB.NMF00P
REFERENCING OLD_TABLE AS OTBL
FOR EACH ROW
MODE DB2SQL
BEGIN ATOMIC
INSERT INTO MYLIB . NMF00PAUDI
SELECT * FROM OTBL
;
END
Becomes this when I view the definition:
BEGIN ATOMIC
INSERT INTO MYLIB . NMF00PAUDI (COL1, COL2, COL3)
SELECT COL1, COL2, COL3 FROM OTBL
;
END
Could you automate the creation of the trigger definition (either with a fancy text templating tool or even just a simple script) that could find the columns from sysibm.syscolumns and generate your trigger body using all the current columns? Then any time the table schema changes you just re-generate the script and re-apply the trigger to cover the new field.
Related
I am trying to create a trigger called 'stockupdate' that updates 'quantity_available' when a new order is placed on my order_line_item_table.
Tables and columns required: stock_table.quantity_available (Represents Stock) order_line_item_table.sale_quantity (Represents each order placed)
I have used the below code to create the trigger, however when I insert a new order to check the trigger is working, the stock_table.quantity_available figure does not update and I am not sure why. Any help would be greatly appreciated.
DELIMITER $$
DROP TRIGGER IF EXISTS stockupdate;
CREATE TRIGGER stockupdate
AFTER INSERT ON order_line_item_table
FOR EACH ROW
BEGIN
UPDATE stock_table
SET quantity_available = (quantity_available - NEW.sale_quantity)
WHERE product_id = NEW.product_id;
END$$
DELIMITER ;
I am expecting the quantity_available to decrease by the sale_quantity amount when a new order is placed on the order_line_item_table
Description:
I am running postgresql 13
I have two tables under different schemas, t1 and t2.
t2 is derivative of t1 in the sense that they share all the same
columns and data, but t2 is always downstream of t1 as far as
validity.
The rows in both tables share the same primary key, which is what I assume would be used as the link between them.
The ask:
I would like to create a trigger that reflects any changes in t1 and syncs t2 to be the same.
I started with INSERT or UPDATE, but if DELETE is easily added, I would like to implement that as well.
Trigger Code:
-- Trigger for t1 to t2 --
CREATE OR REPLACE FUNCTION t1_schema.sync_trigger()
RETURNS TRIGGER AS
$$
BEGIN
INSERT INTO t2_schema.t2 (col1, col2, col3)
VALUES (NEW.col1, NEW.col2, NEW.col3);
RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER t1t2_test_sync
AFTER INSERT OR UPDATE ON t1_schema.t1
FOR EACH ROW
EXECUTE PROCEDURE t1_schema.sync_trigger()
When I execute this code and do a test UPDATE on t1, the same row on t2 does not reflect the changes or give me any errors.
I have tried:
Discretely labeling all rows as updated with NEW. format, but run into the problem of primary key column not being editable in t2.
Adding a WHERE clause after the VALUES clause, something like WHERE primary_key=NEW.primary_key, but I get an error.
Another option I have seen is adding an IF statement before the
INSERT, or adding a WHEN clause in the trigger, but neither have
worked.
Your best approach is to not create t2 as a table. Instead create it as a VIEW on t1. This totally eliminates triggers to keep them synchronized because the actual source is the same. Follows the concept to store a single data point in only 1 place. Keep in mind that if you store a single piece in 2 places, 1 on them will be wrong at some point. (see demo).
create view soq2.t2 as
select *
from soq1.t1;
Also if you need column names to change then use an alias during the create view;
create view soq2.t2a as
select t1_id as t2_id
, name as t2_name
, status as t2_status
from soq1.t1;
(A) Solution based on triggers
You maybe get an error when updating a row in t1 because your trigger function tries to insert a new row in t2 which has alreday been inserted in t2 by the same trigger function when it has been inserted in t1. You need to duplicate and specialize your trigger functions, one for insert, one for update, one for delete because the treatment to be triggered on t2 is different :
CREATE OR REPLACE FUNCTION t1_schema.sync_trigger_insert()
RETURNS TRIGGER AS
$$
BEGIN
INSERT INTO t2_schema.t2 (col1, col2, col3)
VALUES (NEW.col1, NEW.col2, NEW.col3);
RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER t1t2_test_sync_insert
AFTER INSERT ON t1_schema.t1
FOR EACH ROW EXECUTE PROCEDURE t1_schema.sync_trigger_insert() ;
CREATE OR REPLACE FUNCTION t1_schema.sync_trigger_update()
RETURNS TRIGGER AS
$$
BEGIN
UPDATE t2
SET col1 = NEW.col1
, col2 = NEW.col2
, col3 = NEW.col3
WHERE primary_key_t2 = NEW. primary_key_t1 ; -- primary_key_t2 must be replaced by the set of columns which are in the primary key of t2 with AND operators, the same for NEW.primary_key_t1
RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER t1t2_test_sync_update
AFTER UPDATE ON t1_schema.t1
FOR EACH ROW EXECUTE PROCEDURE t1_schema.sync_trigger_update() ;
CREATE OR REPLACE FUNCTION t1_schema.sync_trigger_delete()
RETURNS TRIGGER AS
$$
BEGIN
DELETE FROM t2
WHERE primary_key_t2 = NEW. primary_key_t1 ; -- primary_key_t2 must be replaced by the set of columns which are in the primary key of t2 with AND operators, the same for NEW.primary_key_t1
RETURN OLD; -- NEW is not available for triggers ON DELETE
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER t1t2_test_sync_delete
AFTER DELETE ON t1_schema.t1
FOR EACH ROW EXECUTE PROCEDURE t1_schema.sync_trigger_delete() ;
(B) Solution based on foreign key
It is possible that a foreign key on t2 (col1,col2,col3) referencing t1 (col1, col2, col3) with the options ON UPDATE CASCADE ON DELETE CASCADE may deliver your expected result in a much more simple and efficient way, see the manual. In this case, you don't need the triggers ON UPDATE and ON DELETE anymore, but you still need the trigger ON INSERT.
I am new to PostgreSQL and found a trigger which serves my purpose completely except for one little thing. The trigger is quite generic and runs across different tables and logs different field changes. I found here.
What I now need to do is test for a specific field which changes as the tables change on which the trigger fires. I thought of using substr as all the column will have the same name format e.g. XXX_cust_no but the XXX can change to 2 or 4 characters. I need to log the value in theXXX_cust_no field with every record that is written to the history_ / audit table. Using a bunch of IF / ELSE statements to accomplish this is not something I would like to do.
The trigger as it now works logs the table_name, column_name, old_value, new_value. I however need to log the XXX_cust_no of the record that was changed as well.
Basically you need dynamic SQL for dynamic column names. format helps to format the DML command. Pass values from NEW and OLD with the USING clause.
Given these tables:
CREATE TABLE tbl (
t_id serial PRIMARY KEY
,abc_cust_no text
);
CREATE TABLE log (
id int
,table_name text
,column_name text
,old_value text
,new_value text
);
It could work like this:
CREATE OR REPLACE FUNCTION trg_demo()
RETURNS TRIGGER AS
$func$
BEGIN
EXECUTE format('
INSERT INTO log(id, table_name, column_name, old_value, new_value)
SELECT ($2).t_id
, $3
, $4
,($1).%1$I
,($2).%1$I', TG_ARGV[0])
USING OLD, NEW, TG_RELNAME, TG_ARGV[0];
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER demo
BEFORE UPDATE ON tbl
FOR EACH ROW EXECUTE PROCEDURE trg_demo('abc_cust_no'); -- col name here.
SQL Fiddle.
Related answer on dba.SE:
How to access NEW or OLD field given only the field's name?
List of special variables visible in plpgsql trigger functions in the manual.
How to create a temporary table, if it does not already exist, and add the selected rows to it?
CREATE TABLE AS
is the simplest and fastest way:
CREATE TEMP TABLE tbl AS
SELECT * FROM tbl WHERE ... ;
Do not use SELECT INTO. See:
Combine two tables into a new one so that select rows from the other one are ignored
Not sure whether table already exists
CREATE TABLE IF NOT EXISTS ... was introduced in version Postgres 9.1.
For older versions, use the function provided in this related answer:
PostgreSQL create table if not exists
Then:
INSERT INTO tbl (col1, col2, ...)
SELECT col1, col2, ...
Chances are, something is going wrong in your code if the temp table already exists. Make sure you don't duplicate data in the table or something. Or consider the following paragraph ...
Unique names
Temporary tables are only visible within your current session (not to be confused with transaction!). So the table name cannot conflict with other sessions. If you need unique names within your session, you could use dynamic SQL and utilize a SEQUENCE:
Create once:
CREATE SEQUENCE tablename_helper_seq;
You could use a DO statement (or a plpgsql function):
DO
$do$
BEGIN
EXECUTE
'CREATE TEMP TABLE tbl' || nextval('tablename_helper_seq'::regclass) || ' AS
SELECT * FROM tbl WHERE ... ';
RAISE NOTICE 'Temporary table created: "tbl%"' || ', lastval();
END
$do$;
lastval() and currval(regclass) are instrumental to return the dynamically created table name.
I tried create a trigger:
CREATE TRIGGER DataTrigger ON Data AFTER INSERT , DELETE
AS
BEGIN
IF EXISTS(SELECT * FROM Inserted)
INSERT INTO [dbo].[AuditTrail]
([ActionType]
,[TableName]
,[Name]
,[Time])
('INSERT' //Incorrect syntax near 'INSERT'
,'Data'
,SELECT col1 FROM Inserted
,CURRENT_TIMESTAMP //Incorrect syntax near the keyword 'CURRENT_TIMESTAMP')
END
but it keep saying that I have thoes errors, can somebody show me where I did wrong?
P/S: what is the best way to detect an update?
Thank you
CREATE TRIGGER DataTrigger ON Data AFTER INSERT , DELETE
AS
BEGIN
INSERT INTO [dbo].[AuditTrail]
([ActionType]
,[TableName]
,[Name]
,[Time])
SELECT 'INSERT'
,'Data'
,col1
,CURRENT_TIMESTAMP
FROM Inserted
END
Just for clarification.
Your query, if syntatically correct would have failed if more than one row was inserted, the version above allows for multiple inserts.
The IF EXISTS was redundant, which is why it was removed, if there are no rows there will be no insert into your audit table.
If you want to audit DELETE you'll need a similar statement again, but using the Deleted table rather than Inserted
To audit UPDATE, create a new trigger, for each updated row you get an entry in Inserted with the new updates and an entry in Deleted with the old data, you can join these if you want to track old and new.
CREATE TRIGGER DataTrigger ON Data AFTER INSERT , DELETE
AS
BEGIN
INSERT INTO [dbo].[AuditTrail]
([ActionType]
,[TableName]
,[Name]
,[Time])
SELECT 'INSERT', 'Data', col1, CURRENT_TIMESTAMP FROM Inserted
END
I am not entirely sure what you are trying to accomplish here so I have just corrected the syntax to get it to work, but it should help. I have also tried to handle the deleted case as well
CREATE TRIGGER DataTrigger
ON Data
AFTER INSERT , DELETE
AS
BEGIN
INSERT INTO [dbo].[AuditTrail]([ActionType],
[TableName],
[Name],
[Time])
SELECT 'INSERT','Data', col1, CURRENT_TIMESTAMP
FROM Inserted
INSERT INTO [dbo].[AuditTrail]([ActionType],
[TableName],
[Name],
[Time])
SELECT 'DELETE','Data', col1, CURRENT_TIMESTAMP
FROM Deleted
END