SQL - trigger insert only when a user made an update - sql-server

I have a table, customers_accounts, that tracks some basic information about a customer on an account. When the customer's file is opened, I synchronize the information from an external system so our user gets the most updated information.
UPDATE
customers_accounts
SET
first_name = 'bob',
last_name = 'burger'
WHERE
account_number = '12345'
When a user updates the account, I do the same query, but I update a column indicating the last user to make the change
UPDATE
customers_accounts
SET
first_name = 'bob',
last_name = 'burger',
updated_by = 'H Jon Benjamin',
updated_on = GETDATE()
WHERE
account_number = '12345'
Here's the problem I'm trying to solve. I want to track changes in a history table, but only log changes when they're made by a user, not if they're from the external system. So my plan was to create a trigger that inserts a row if the user column is not blank on the insert (since the updated_by is implicitly null above in the first update)
What I tried is this:
ALTER trigger [dbo].[Accounts_Customers_LogUpdate]
ON [dbo].[Accounts_Customers]
AFTER UPDATE
AS
DECLARE #Now AS DATETIME = GETDATE()
DECLARE #User AS NVARCHAR(150)
SELECT #User = (SELECT [updated_by] FROM INSERTED)
IF (#User IS NOT NULL)
BEGIN
SET NOCOUNT ON
INSERT INTO [dbo].[Accounts_Customers-History]
SELECT *, #User, #Now
FROM inserted
END
Accounts_Customers-History is an exact copy of the table with two additional columns, change_made_by and change_made_on
It doesn't behave how i'd expect though. It inserts whatever value is in updated_by into change_made_by, regardless of the value of updated_by in the query. So I'm getting logged activity triggered by both the user and the import.

Use UPDATE() for this:
Returns a Boolean value that indicates whether an INSERT or UPDATE attempt was made on a specified column of a table or view. UPDATE() is used anywhere inside the body of a Transact-SQL INSERT or UPDATE trigger to test whether the trigger should execute certain actions.
This means that the update function will return false for the first update statement in the question, and true for the second update statement - which is exactly what you need.
Also, please note you should always specify the columns list in an insert statement,
and also always specify the columns list in a select statement. (Why?)
A revised version of your trigger might look something like this:
ALTER TRIGGER [dbo].[Accounts_Customers_LogUpdate]
ON [dbo].[Accounts_Customers]
AFTER UPDATE
AS
DECLARE #Now as DateTime = GetDate()
IF UPDATE(updated_by)
BEGIN
-- Always specify the columns list in an insert statement!
insert into [dbo].[Accounts_Customers-History] (<Columns list>)
-- Always specify the columns list in a select statement!
select <columns list>, #Now
from inserted
END
Please note that the UPDATE() function does not give you any indication if the insert or update statement that fired the trigger was successful, nor does it give you an indication if the value of the column has actually changed - it only indicates whether that column was a part of the insert or update statement that fired the trigger - as you can read in the last paragraph of the remarks section:
If a trigger applies to a column, the UPDATED value will return as true or 1, even if the column value remains unchanged. This is by-design, and the trigger should implement business logic that determines if the insert/update/delete operation is permissible or not.

Related

Set a temporary global variable during a Postgres query

Is it possible to set a variable during a query (valid only for the query in question) that can be captured by a TRIGGER procedure?
For example, I want to record the ID of the executor of a query (current_user is always the same).
So I would do something like this:
tbl_executor (
id PRIMARY KEY,
name VARCHAR
);
tbl_log (
executor REFERENCE tbl_executor(id),
op VARCHAR
);
tbl_other ...
CREATE TRIGGER t AFTER INSERT OR UPDATE OR DELETE ON tbl_executor
FOR EACH ROW
EXECUTE PROCEDURE (INSERT INTO tbl_log VALUES( ID_VAR_OF_THIS_QUERY ,TG_OP))
Now if I run a query like:
INSERT INTO tbl_other
VALUES(.......) - and set ID_VAR_OF_THIS_QUERY='id of executor' -
I get the following result:
tbl_log
-----------------------------
id | op |
-----------------------------
'id of executor' | 'INSERT'|
I hope I have made the idea... and I think it is hardly feasible... but is there anyone who could help me?
To answer the question
You can SET a (customized option) like this:
SET myvar.role_id = '123';
But that requires a literal value. There is also the function set_config(). Quoting the manual:
set_config(setting_name, new_value, is_local) ... set parameter and return new value
set_config sets the parameter setting_name to new_value. If is_local is true, the new value will only apply to the current transaction.
Correspondingly, read option values with SHOW or current_setting(). Related:
How to use variable settings in trigger functions?
But your trigger is on the wrong table (tbl_executor) with wrong syntax. Looks like Oracle code, where you can provide code to CREATE TRIGGER directly. In Postgres you need a trigger function first:
How to use PostgreSQL triggers?
So:
CREATE OR REPLACE FUNCTION trg_log_who()
RETURNS trigger AS
$func$
BEGIN
INSERT INTO tbl_log(executor, op)
VALUES(current_setting('myvar.role_id')::int, TG_OP); -- !
RETURN NULL; -- irrelevant for AFTER trigger
END
$func$ LANGUAGE plpgsql;
Your example setup requires the a type cast ::int.
Then:
CREATE TRIGGER trg_log_who
AFTER INSERT OR UPDATE OR DELETE ON tbl_other -- !
FOR EACH ROW EXECUTE PROCEDURE trg_log_who(); -- !
Finally, fetching id from the table tbl_executor to set the variable:
BEGIN;
SELECT set_config('myvar.role_id', id::text, true) -- !
FROM tbl_executor
WHERE name = current_user;
INSERT INTO tbl_other VALUES( ... );
INSERT INTO tbl_other VALUES( ... );
-- more?
COMMIT;
Set the the third parameter (is_local) of set_config() to true to make it session-local as requested. (The equivalent of SET LOCAL.)
But why per row? Would seem more reasonable to make it per statement?
...
FOR EACH STATEMENT EXECUTE PROCEDURE trg_foo();
Different approach
All that aside, I'd consider a different approach: a simple function returning the id a column default:
CREATE OR REPLACE FUNCTION f_current_role_id()
RETURNS int LANGUAGE sql STABLE AS
'SELECT id FROM tbl_executor WHERE name = current_user';
CREATE TABLE tbl_log (
executor int DEFAULT f_current_role_id() REFERENCES tbl_executor(id)
, op VARCHAR
);
Then, in the trigger function, ignore the executor column; will be filled automatically:
...
INSERT INTO tbl_log(op) VALUES(TG_OP);
...
Be aware of the difference between current_user and session_user. See:
How to check role of current PostgreSQL user from Qt application?
One option is to create a shared table to hold this information. Since it's per-connection, the primary key should be pg_backend_pid().
create table connection_global_vars(
backend_pid bigint primary key,
id_of_executor varchar(50)
);
insert into connection_global_vars(backend_pid) select pg_backend_pid() on conflict do nothing;
update connection_global_vars set id_of_executor ='id goes here' where backend_pid = pg_backend_pid();
-- in the trigger:
CREATE TRIGGER t AFTER INSERT OR UPDATE OR DELETE ON tbl_executor
FOR EACH ROW
EXECUTE PROCEDURE (INSERT INTO tbl_log VALUES( (select id_of_executor from connection_global_vars where backend_pid = pg_backend_pid()) ,TG_OP))
Another option is to create a temporary table (which exists per-connection).
create temporary table if not exists connection_global_vars(
id_of_executor varchar(50)
) on commit delete rows;
insert into connection_global_vars(id_of_executor) select null where not exists (select 1 from connection_global_vars);
update connection_global_vars set id_of_executor ='id goes here';
-- in the trigger:
CREATE TRIGGER t AFTER INSERT OR UPDATE OR DELETE ON tbl_executor
FOR EACH ROW
EXECUTE PROCEDURE (INSERT INTO tbl_log VALUES( (select id_of_executor from connection_global_vars where backend_pid = pg_backend_pid()) ,TG_OP))
For PostgreSQL in particular it probably won't make much difference to performance, except an unlogged temporary table may just possibly be slightly faster.
If you have performance issues around not recognising that it's a single row-table, you might run analyse.

Recording info in SQL Server trigger

I have a table called dsReplicated.matDB and a column fee_earner. When that column is updated, I want to record two pieces of information:
dsReplicated.matDB.mt_code
dsReplicated.matDB.fee_earner
from the row where fee_earner has been updated.
I've got the basic syntax for doing something when the column is updated but need a hand with the above to get this over the line.
ALTER TRIGGER [dsReplicated].[tr_mfeModified]
ON [dsReplicated].[matdb]
AFTER UPDATE
AS
BEGIN
IF (UPDATE(fee_earner))
BEGIN
print 'Matter fee earner changed to '
END
END
The problem with triggers in SQL server is that they are called one per SQL statement - not once per row. So if your UPDATE statement updates 10 rows, your trigger is called once, and the Inserted and Deleted pseudo tables inside the trigger each contain 10 rows of data.
In order to see if fee_earner has changed, I'd recommend using this approach instead of the UPDATE() function:
ALTER TRIGGER [dsReplicated].[tr_mfeModified]
ON [dsReplicated].[matdb]
AFTER UPDATE
AS
BEGIN
-- I'm just *speculating* here what you want to do with that information - adapt as needed!
INSERT INTO dbo.AuditTable (Id, TriggerTimeStamp, Mt_Code, Old_Fee_Earner, New_Fee_Earner)
SELECT
i.PrimaryKey, SYSDATETIME(), i.Mt_Code, d.fee_earner, i.fee_earner
FROM Inserted i
-- use the two pseudo tables to detect if the column "fee_earner" has
-- changed with the UPDATE operation
INNER JOIN Deleted d ON i.PrimaryKey = d.PrimaryKey
AND d.fee_earner <> i.fee_earner
END
The Deleted pseudo table contains the values before the UPDATE - so that's why I take the d.fee_earner as the value for the Old_Fee_Earner column in the audit table.
The Inserted pseudo table contains the values after the UPDATE - so that's why I take the other values from that Inserted pseudo-table to insert into the audit table.
Note that you really must have an unchangeable primary key in that table in order for this trigger to work. This is a recommended best practice for any data table in SQL Server anyway.

SQL server GetDate in trigger called sequentially has the same value

I have a trigger on a table for insert, delete, update that on the first line gets the current date with GetDate() method.
The trigger will compare the deleted and inserted table to determine what field has been changed and stores in another table the id, datetime and the field changed. This combination must be unique
A stored procedure does an insert and an update sequentially on the table. Sometimes I get a violation of primary key and I suspect that the GetDate() returns the same value.
How can I make the GetDate() return different values in the trigger.
EDIT
Here is the code of the trigger
CREATE TRIGGER dbo.TR
ON table
FOR DELETE, INSERT, UPDATE
AS
BEGIN
SET NoCount ON
DECLARE #dt Datetime
SELECT #dt = GetDate()
insert tableLog (id, date, field, old, new)
select I.id, #dt, 'field', D.field, I.field
from INSERTED I LEFT JOIN DELETED D ON I.id=D.id
where IsNull(I.field, -1) <> IsNull(D.field, -1)
END
and the code of the calls
...
insert into table ( anotherfield)
values (#anotherfield)
if ##rowcount=1 SET #ID=##Identity
...
update table
set field = #field
where Id = #ID
...
Sometimes the GetDate() between the 2 calls (insert and update) takes 7 milliseconds and sometimes it has the same value.
That's not exactly full solution but try using SYSDATETIME instead and of course make sure that target table can store up datetime2 up to microseconds.
Note that you can't force different datetime regardless of precision (unless you will start counting up to ticks) as stuff can just happen at the same time wihthin given precision.
If stretching up to microseconds won't solve the issue on practical level, I think you will have to either redesign this logging schema (perhaps add identity column on top of what you have) or add some dirty trick - like make this insert in try catch block and add like microsecond (nanosecond?) in a loop until you insert successfully. Definitely not s.t. I would recommend.
Look at this answer: SQL Server: intrigued by GETDATE()
If you are inserting multiple ROWS, they will all use the same value of GetDate(), so you can try wrapping it in a UDF to get unique values. But as I said, this is just a guess unless you post the code of your trigger so we can see what you are actually doing?
It sounds like you're trying to create an audit trail - but now you want to forge some of the entries?
I'd suggest instead adding a rowversion column to the table and including that in your uniqueness criteria - either instead of or as well as the datetime value that is being recorded.
In this way, even if two rows are inserted with identical date/time data, you can still tell the actual insertion order.

How to store existing column value in new table using trigger

I have two tables
Customer
CustomerUpdate
Structure of both tables are like this
Customer table's structure
CustomerName | CustomerId
CustomerUpdate table's structure
NewCustomerName | NewCustomerId | OldCustomerName
I have few values inserted in the Customer table. Whenever I should update the data in this table I want that the existing as well as new data should be triggered into new table CustomerUpdate.
For this I created a trigger but this is only pulling the updated data, it's not pulling the existing data..
CREATE TRIGGER trgAfterUpdate
ON [dbo].Customer
FOR UPDATE
AS
SET NOCOUNT ON
declare #NewCustomerName nchar(20);
declare #NewCustomerId nchar(20);
declare #OldCustomerName nchar(20);
declare #audit_action varchar(100);
select #NewCustomerName = i.CustomerName from inserted i;
select #NewCustomerId = i.CustomerId from inserted i;
select #OldCustomerName = c.CustomerName
from Customer c
where CustomerId = #NewCustomerId;
if update(CustomerName)
set #audit_action='Updated Record -- After Update Trigger.';
if update(CustomerId)
set #audit_action='Updated Record -- After Update Trigger.';
insert into CustomerUpdate(NewCustomerName, NewCustomerId, OldCustomername)
values(#NewCustomerName, #NewCustomerId, #OldCustomerName);
PRINT 'AFTER UPDATE Trigger fired.'
GO
Please help me out
First, selecting from the table being modified when an update trigger is executing will get the new value. These are AFTER triggers (rather than INSTEAD triggers) and therefore the update has already happened by the time the trigger fires (although it can be rolled back). If you need the old value, you should select from the DELETED pseudo-table.
Second, as pointed out by #marc_s in comments, your trigger has the hidden assumption that only one row is affected by each update. This may very well be a valid assumption for your environment, if your application only ever updates one row at a time, but in the general case, every trigger should be ready to handle the case where many rows are affected by a single update. Writing your triggers to handle multiple rows is good practice.
Third, all of your sequentially executing code is pretty much unnecessary. The old value and the new value can be retrieved and inserted all at once:
CREATE TRIGGER trgAfterUpdate
ON [dbo].Customer
FOR UPDATE
AS
BEGIN
SET NOCOUNT ON
insert into CustomerUpdate(NewCustomerName, NewCustomerId, OldCustomername)
-- case 1: ID unchanged
SELECT I.CustomerName, I.CustomerID, D.CustomerName
FROM Inserted I
JOIN Deleted D on I.CustomerID=D.CustomerID
UNION ALL
-- case 2: ID changed, Name unchanged
SELECT I.CustomerName, I.CustomerID, D.CustomerName
FROM Inserted I
JOIN Deleted D on I.CustomerName=D.CustomerName
WHERE I.CustomerID<>D.CustomerID
UNION ALL
--case 3: ID changed, Name changed
SELECT I.CustomerName, I.CustomerID, D.CustomerName
FROM Inserted I
LEFT JOIN Deleted D on I.CustomerID=D.CustomerID OR I.CustomerName=D.CustomerName
WHERE D.CustomerID IS NULL;
END

SQL Server timestamping trigger

My knowledge of SQL is pretty limited as I mostly focus in backend Ruby development. However, due to architectural changes and wanting to keep things well designed; I have decided to set up timestamping on the database level rather than on the backend level.
As it goes right now, all of my tables have two columns: CreatedAt and UpdatedAt, both with a default value of GETDATE().
However, I now need to set up a timestamping trigger for UpdatedAt, so that every time a row (or rows) are updated, the UpdatedAt column for those rows gets a brand new timestamp.
I am having trouble with the following trigger I wrote. I am getting an error:
Incorrect Syntax near '='
I am testing out my trigger on my Orders table first, and then I plan to move the functionality to all tables.
CREATE TRIGGER dbo.trgTimestampAfterUpdate
ON Dbo.Orders
AFTER UPDATE
AS
BEGIN
IF EXISTS (SELECT * FROM inserted)
BEGIN
SET UpdatedAt = GETDATE()
END
END
I know that I can access the inserted, and deleted virtual tables when using a trigger. My thought with this query was that I would use inserted in order to distinguish which rows have been updated. If anyone can help that would be great, and also if you wouldn't mind explaining to me what I messed up with my syntax or line of thinking would be greatly appreciated.
You can't really access the inserted tables quite like that. Having just Set UpdatedAt =... is an incomplete statement. Implicitly it makes sense to you but even in your trigger, you have to make complete SQL statements.
The way to do this is to JOIN to the INSERTED table (in the example below, I'm using a semi-join) You can then use the contents of the INSERTED table to perform another update.
CREATE TRIGGER [dbo].[trgTimestampAfterUpdate] ON dbo.orders
FOR UPDATE
AS
BEGIN
IF NOT(UPDATE(UpdatedAt)) --Avoid triggers firing triggers
BEGIN
UPDATE dbo.orders
SET UpdatedAt = GETDATE()
WHERE id IN ( SELECT id
FROM inserted )
END
END
Two REALLY important things to note in this code example. First Updating the table with the trigger on it will cause the trigger to fire again (creating a loop that will increase until you reach the max level of nested triggers on your system.) I put a check to make sure it terminates if you're only updating the updatedat column.
Second, never ever assume there is only one row in the inserted table. Something like the code below is a very common mistake
DECLARE #id INT
SELECT #id = id FROM INSERTED
UPDATE MyTable
SET UpdatedAT = GETDATE()
WHERE id = #id
--DON'T DO THIS!
This looks right, and is a common mistake, but it will only ever update 1 record and there could be any number of records in the INSERTED table.

Resources