I want to create a trigger to check what is being deleted against business rules and then cancel the deletion if needed. Any ideas?
The solution used the Instead of Delete trigger. The Rollback tran stopped the delete. I was afraid that I would have a cascade issue when I did the delete but that didn't seem to happen. Maybe a trigger cannot trigger itself.
Use an INSTEAD OF DELETE (see MSDN) trigger and decide within the trigger what you really want to do.
The solution used the Instead of Delete trigger. The Rollback tran stopped the delete. I was afraid that I would have a cascade issue when I did the delete but that did'nt seem to happen. Maybe a trigger cannot trigger itself. Anyhow, thanks all for your help.
ALTER TRIGGER [dbo].[tr_ValidateDeleteForAssignedCalls]
on [dbo].[CAL]
INSTEAD OF DELETE
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
DECLARE #RecType VARCHAR(1)
DECLARE #UserID VARCHAR(8)
DECLARE #CreateBy VARCHAR(8)
DECLARE #RecID VARCHAR(20)
SELECT #RecType =(SELECT RecType FROM DELETED)
SELECT #UserID =(SELECT UserID FROM DELETED)
SELECT #CreateBy =(SELECT CreateBy FROM DELETED)
SELECT #RecID =(SELECT RecID FROM DELETED)
-- Check to see if the type is a Call and the item was created by a different user
IF #RECTYPE = 'C' and not (#USERID=#CREATEBY)
BEGIN
RAISERROR ('Cannot delete call.', 16, 1)
ROLLBACK TRAN
RETURN
END
-- Go ahead and do the update or some other business rules here
ELSE
Delete from CAL where RecID = #RecID
END
The trigger can roll back the current transaction, which will have the effect of cancelling the deletion. As the poster above also states, you can also use an instead of trigger.
According to MSDN documentation about INSTEAD OF DELETE triggers:
The deleted table sent to a DELETE
trigger contains an image of the rows
as they existed before the DELETE
statement was issued.
If I understand it correctly the DELETE is actually being executed. What am I missing?
Anyway, I don't understand why do you want to delete the records and if the business rules are not passed then undelete those records. I would have swear it should be easier to test if you pass the business rules before deleting the records.
And I would have said use a transaction, I haven't heard before about INSTEAD OF triggers.
Related
I have two tables in this scenario. One is my "hot sync" table which is near-realtime bi-directional sync of data from my Salesforce Org to a Postgres table. As data changes in the source system (Salesforce), it updates that table on Postgres.
On this table in Postgres, I have a trigger that runs some logic. It basically checks to see if the record triggering it has a sent date that meets some business logic, copy that row into another schema/table to "archive" it.
This all works fine.
What I need to do however is once this row has been copied into the other table, I need to update the status of the record hot sync table. Since it is bi-directional, this will allow the data in Salesforce to reflect the changes I make from the Postgres side.
Can I place this update statement within the originating trigger or is this going to cause recursion issues?
CREATE FUNCTION salesforce.archivelogicfunc()
RETURNS trigger
LANGUAGE 'plpgsql'
COST 100
VOLATILE NOT LEAKPROOF
AS $BODY$ BEGIN
IF (DATE(NEW.et4ae5__datesent__c) < NOW() - INTERVAL '180 days'
AND DATE(NEW.et4ae5__datesent__c) > NOW() - INTERVAL '540 days')
THEN
INSERT INTO archive.individualemailresult__c
(dateopened__c,
numberoftotalclicks__c,
datebounced__c,
fromname__c,
hardbounce__c,
fromaddress__c,
softbounce__c,
name,
lastmodifieddate,
opened__c,
ownerid,
subjectline__c,
isdeleted,
contact__c,
systemmodstamp,
lastmodifiedbyid,
datesent__c,
dateunsubscribed__c,
createddate,
createdbyid,
lead__c,
tracking_as_of__c,
numberofuniqueclicks__c,
senddefinition__c,
mergeid__c,
triggeredsenddefinition__c,
sfid,
id,
_hc_lastop,
_hc_err,
isarchived)
VALUES
(NEW.et4ae5__dateopened__c,
NEW.et4ae5__numberoftotalclicks__c,
NEW.et4ae5__datebounced__c,
NEW.et4ae5__fromname__c,
NEW.et4ae5__hardbounce__c,
NEW.et4ae5__fromaddress__c,
NEW.et4ae5__softbounce__c,
NEW.name,
NEW.lastmodifieddate,
NEW.et4ae5__opened__c,
NEW.ownerid,
NEW.et4ae5__subjectline__c,
NEW.isdeleted,
NEW.et4ae5__contact__c,
NEW.systemmodstamp,
NEW.lastmodifiedbyid,
NEW.et4ae5__datesent__c,
NEW.et4ae5__dateunsubscribed__c,
NEW.createddate,
NEW.createdbyid,
NEW.et4ae5__lead__c,
NEW.et4ae5__tracking_as_of__c,
NEW.et4ae5__numberofuniqueclicks__c,
NEW.et4ae5__senddefinition__c,
NEW.et4ae5__mergeid__c,
NEW.et4ae5__triggeredsenddefinition__c,
NEW.sfid,
NEW.id,
NEW._hc_lastop,
NEW._hc_err,
NEW.isarchived__c)
ON CONFLICT (id)
DO NOTHING;
-- Update SF to reflect the archive
UPDATE salesforce."et4ae5__individualemailresult__c" SET isarchived__c = true, isdeleted = true WHERE id = NEW.id;
END IF;
RETURN NULL;
END;
$BODY$;
ALTER FUNCTION salesforce.archivelogicfunc()
OWNER TO ....;
My understanding is that the NEW.* is only going to contain the rows that caused the trigger to fire in the first place. Therefore if my trigger was fired for a single record, the update statement NEW.id should only update one record on the source table?
Trying to ensure the trigger isn't going to fire again with the update statement causing some recursive loop that I am not expecting.
My concern is:
Record is Updated
Trigger Fires and inserts record into an archive table
Update runs on the source table to update the record for the new.id
This update causes the trigger to run again. The insert would fail due to the on conflict, but the update would then run again, and again etc..
The original trigger is fired AFTER INSERT/UPDATE.
TRIGGER:
CREATE TRIGGER archivelogic_firetrigger
AFTER INSERT OR UPDATE
ON salesforce.et4ae5__individualemailresult__c
FOR EACH ROW
EXECUTE PROCEDURE salesforce.archivelogicfunc();
UPDATE:
I added a WHEN condition to my trigger. It appeared to work on a basic test, but willing to take any other advice if suggested.
CREATE TRIGGER archivelogic_firetrigger
AFTER INSERT OR UPDATE
ON salesforce.et4ae5__individualemailresult__c
FOR EACH ROW
WHEN (pg_trigger_depth() = 0) // <-- Added to prevent recursion
EXECUTE PROCEDURE salesforce.archivelogicfunc();
The easiest would be to make it a before trigger, and to replace the update by
NEW.isarchived__c = true;
NEW. isdeleted = true;
[...]
RETURN NEW;
Otherwise, you can filter the rows before running the trigger: it will be called only when isarchived__c and isdeleted have NOT changed (it may be dangerous though, just imagine someone updating ALL fields)
CREATE TRIGGER archivelogic_firetrigger
AFTER INSERT OR UPDATE
ON salesforce.et4ae5__individualemailresult__c
FOR EACH ROW
WHEN (NEW.isarchived__c IS NOT DISTINCT FROM OLD.isarchived__c
AND NEW.isdeleted IS NOT DISTINCT FROM OLD.isdeleted )
EXECUTE PROCEDURE salesforce.archivelogicfunc();
How to prevent or avoid running update or delete statements without where clauses in PostgreSQL?
Same as SQL_SAFE_UPDATES statement in MySQL is needed for PostgreSQL.
For example:
UPDATE table_name SET active=1; -- Prevent this statement or throw error message.
UPDATE table_name SET active=1 WHERE id=1; -- This is allowed
My company database has many users with insert and update privilege any one of the users do that unsafe update.
In this secoario how to handle this.
Any idea can write trigger or any extension to handle the unsafe update in PostgreSQL.
I have switched off autocommits to avoid these errors. So I always have a transaction that I can roll back. All you have to do is modify .psqlrc:
\set AUTOCOMMIT off
\echo AUTOCOMMIT = :AUTOCOMMIT
\set PROMPT1 '%[%033[32m%]%/%[%033[0m%]%R%[%033[1;32;40m%]%x%[%033[0m%]%# '
\set PROMPT2 '%[%033[32m%]%/%[%033[0m%]%R%[%033[1;32;40m%]%x%[%033[0m%]%# '
\set PROMPT3 '>> '
You don't have to insert the PROMPT statements. But they are helpful because they change the psql prompt to show the transaction status.
Another advantage of this approach is that it gives you a chance to prevent any erroneous changes.
Example (psql):
database=# SELECT * FROM my_table; -- implicit start transaction; see prompt
-- output result
database*# UPDATE my_table SET my_column = 1; -- missed where clause
UPDATE 525125 -- Oh, no!
database*# ROLLBACK; -- Puh! revert wrong changes
ROLLBACK
database=# -- I'm completely operational and all of my circuits working perfectly
There actually was a discussion on the hackers list about this very feature. It had a mixed reception, but might have been accepted if the author had persisted.
As it is, the best you can do is a statement level trigger that bleats if you modify too many rows:
CREATE TABLE deleteme
AS SELECT i FROM generate_series(1, 1000) AS i;
CREATE FUNCTION stop_mass_deletes() RETURNS trigger
LANGUAGE plpgsql AS
$$BEGIN
IF (SELECT count(*) FROM OLD) > TG_ARGV[0]::bigint THEN
RAISE EXCEPTION 'must not modify more than % rows', TG_ARGV[0];
END IF;
RETURN NULL;
END;$$;
CREATE TRIGGER stop_mass_deletes AFTER DELETE ON deleteme
REFERENCING OLD TABLE AS old FOR EACH STATEMENT
EXECUTE FUNCTION stop_mass_deletes(10);
DELETE FROM deleteme WHERE i < 100;
ERROR: must not modify more than 10 rows
CONTEXT: PL/pgSQL function stop_mass_deletes() line 1 at RAISE
DELETE FROM deleteme WHERE i < 10;
DELETE 9
This will have a certain performance impact on deletes.
This works from v10 on, when transition tables were introduced.
If you can afford making it a little less convinient for your users, you might try revoking UPDATE privilege for all "standard" users and creating a stored procedure like this:
CREATE FUNCTION update(table_name, col_name, new_value, condition) RETURNS void
/*
Check if condition is acceptable, create and run UPDATE statement
*/
LANGUAGE plpgsql SECURITY DEFINER
Because of SECURITY DEFINER this way your users will be able to UPDATE despite not having UPDATE privilege.
I'm not sure if this is a good approach, but this way you can force as strict UPDATE (or anything else) requirements as you wish.
Of course the more complicated UPDATES are required, the more complicated has to be your procedure, but if this is mostly just about updating single row by ID (as in your example) this might be worth a try.
Hi I'm up to develop a simple audit trigger for postgresql server. According to this document, I pretty much understand how it works. But I want to record my activity only when the certain row is updated. Below is the code from the link. And it records when there is update no matter what row is updated.
IF (TG_OP = 'UPDATE') THEN
...
Please help me how to give a condition to above code. Thanks!
The trigger is written in PL/PgSQL. I strongly suggest you study the PL/PgSQL manual if you're going to modify PL/PgSQL code.
In triggers, the row data is in OLD and NEW (for UPDATE triggers). So you can do IF tests on that like anything else. E.g.:
IF (TG_OP = 'UPDATE') THEN
IF NEW."name" = 'name_to_audit' OR OLD."name" = 'name_to_audit' THEN
-- do audit commands
END IF;
END IF;
Both NEW and OLD are tested in case the name is being changed from/to the name of interest.
In this case you could instead change it to use a WHEN clause on the CREATE TRIGGER, so you never fire the trigger at all unless the conditions to audit are met. See the WHEN clause on triggers.
This is just a basic programming problem; you'll need to learn the programming language in order to use it.
See also the updated trigger for Pg 9.1.
Oh, and remember to think about NULL; remember NULL = 'anything' is NULL. Use IS DISTINCT FROM if you want to say "these things are equal, or are both null".
From Postgresql Docs:
CREATE TRIGGER log_update
AFTER UPDATE ON accounts
FOR EACH ROW
WHEN (OLD.* IS DISTINCT FROM NEW.*)
EXECUTE PROCEDURE log_account_update();
This only work for UPDATE on that table. For INSERT AND DELETE you can use same without WHERE query. Hope this help others.
Is there a way to prevent deletion of all rows in a table? I know how to user DDL triggers to prevent dropping a table or truncation. I once failed to highlight the entire commmand, leaving off the where clause, and deleted all rows in a table. I want to prevent this from occurring again unless a where clause is present.
You could do this with a trigger.
CREATE TRIGGER dbo.KeepRows
ON dbo.TableName
FOR DELETE
AS
BEGIN
SET NOCOUNT ON;
IF NOT EXISTS (SELECT 1 FROM dbo.TableName)
BEGIN
RAISERROR('Naughty developer, you tried to delete the last row!', 11, 1);
ROLLBACK TRANSACTION;
END
END
GO
In order to avoid the delete and then rollback, which can be quite expensive if the table is large, you could instead implement this as an instead of trigger, which will prevent any work from happening (except the check of course). This assumes the column id is the primary key:
CREATE TRIGGER dbo.KeepRows2
ON dbo.TableName
INSTEAD OF DELETE
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (SELECT 1 FROM dbo.TableName WHERE id NOT IN (SELECT id FROM deleted))
BEGIN
DELETE dbo.TableName WHERE id IN (SELECT id FROM deleted);
END
-- optional:
ELSE
BEGIN
RAISERROR('Nice try, you need a WHERE clause!', 11, 1);
END
END
GO
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