Set a temporary global variable during a Postgres query - database

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.

Related

SQL - trigger insert only when a user made an update

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.

T-SQL Increment Id after Insert

I'm currently working on a stored procedure in SQL Server 2012 using T-SQL. My problem: I have several SWOTs (e.g. for a specific client) holding several SWOTParts (strengths, weaknesses, opportunities, and threats). I store the values in a table Swot as well as in another table SwotPart.
My foreign Key link is SwotId in SwotPart, thus 1 Swot can hold N SwotParts. Hence, I store the SwotId in every SwotPart.
I can have many Swots and now need to set the SwotId correctly to create the foreign key. I set the SwotId using SCOPE_IDENTITY() unfortunately it only takes the last SwotId from the DB.I'm looking for something like a for loop to increment the SwotId after each insert on the 1st insert.
DECLARE #SwotId INT = 1;
-- 1st insert
SET NOCOUNT ON
INSERT INTO [MySchema].[SWOT]([SwotTypeId]) // Type can be e.g. a sepcific client
SELECT SwotTypeId
FROM #SWOTS
SET #SwotId = SCOPE_IDENTITY(); // currently e.g. 7, but should increment: 1, 2, 3...
-- 2nd insert
SET NOCOUNT ON
INSERT INTO [MySchema].[SwotPart]([SwotId], [FieldTypeId], [Label]) // FieldType can be e.g. Streangh
SELECT #SwotId, FieldTypeId, Label
FROM #SWOTPARTS
Do you know how to solve this issue? What could I use instead of SCOPE_IDENTITY()?
Thank you very much!
You can output the inserted rows into a temporary table, then join your #swotparts to the temporary table based on the natural key (whatever unique column set ties them together beyond the SwotId). This would solve the problem with resorting to loops or cursors, while also overcoming the obstacle of doing a single swot at a time.
set nocount, xact_abort on;
create table #swot (SwotId int, SwotTypeId int);
insert into MySchema.swot (SwotTypeId)
output inserted.SwotId, inserted.SwotTypeId into #swot
select SwotTypeId
from #swots;
insert into MySchema.SwotPart(SwotId, FieldTypeId, Label)
select s.SwotId, p.FieldTypeId, p.Label
from #swotparts p
inner join #swot s
on p.SwotTypeId = p.SwotTypeId;
Unfortunately I cant comment so I`ll leave you an answer hopefully to clarify some things:
Since you need to create the correct foreign key I don`t understand
why do you need to increment a value instead of using the id inserted
into the SWOT table.
I suggest returning the inserted id using the SCOPE_IDENTITY right after the insert statement and use it for you insert into the swot parts (there is plenty of info about it and how to use it)
DECLARE #SwotId INT;
-- 1st insert
INSERT INTO [MySchema].[SWOT]([SwotTypeId]) // Type can be e.g. a sepcific client
SET #SwotId = SCOPE_IDENTITY();
-- 2nd insert
INSERT INTO [MySchema].[SwotPart]([SwotId], [FieldTypeId], [Label])
SELECT #SwotId, FieldTypeId, Label
FROM #SWOTPARTS

One table return value to insert another table as a column Oracle

I have a Oracle trigger and I need to create column after insert rows to first table.
So.. In my scenario:
When some record inserted into NEWS_TBL and i need to get that(in here i get it via last inserted record) and i need to get the NAME from the NEWS_TBL and returning value to NewsName variable and that returned value inserted to the NEWS_TYPE_TBL as a Column.
Below code is not working. can anyone pls give me a solution for this.
MyCode
BEFORE DELETE OR INSERT OR UPDATE
ON NEWS.NEWS_FIRST
REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
DECLARE
NewsName varchar2(50);
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
EXECUTE IMMEDIATE
'select *
from ( select a.NAME,a.ID, max(ID) over () as max_pk
from NEWS_TBL a)
where ID = max_pk
RETURNING NAME INTO NewsName';
'ALTER TABLE NEWS_TYPE_TBL ADD [NewsName] NUMBER(50) NULL';
// I want to add returning ticket name to here from 1st query.
END trigNews;
/

TSQL Trigger unknown Column

I have a table which have 31 Columns
PrimaryField,PersNr, a1, a2, a3, a4, ...
in the a1 - a31 fields are values. The software generate automaticly each month one row.
The user can now from the software update a field or delete it (Update to NULL).
But now I need a Trigger which make an insert in a new table for each changed field. The Problem is the comparison from the before row and the changed row. How can I get the changed field when the User Update the Row?
Here is some code you can run that should work:
CREATE TRIGGER TriggerName --The name of your trigger
ON Table1 --The table where the update happens
AFTER UPDATE
AS
INERT INTO --you new tabe namegoes here
()--your new table fields go here
SELECT --only the fields you have in the parenthesis above in the same order
FROM Inerted I
The way most triggers are is that as soon as a value is inseted updated or deleted the trigger has a special vlaue in this case fr update and inserted actions the table is called Inserted and that will grab the most current value entered.
In a SQL Server trigger, you have access to Inserted and Deleted pseudo tables; those keep the data that has been modified: the first one stores the new values, the second the old ones.
CREATE TRIGGER [dbo].[MyTableUpdate
ON [dbo].[MyTable]
AFTER UPDATE
AS
IF ##ROWCOUNT = 0
RETURN
IF NOT UPDATE (a1) AND NOT UPDATE(a2) AND NOT UPDATE(a3) AND NOT UPDATE...
RETURN
SELECT YourCOlumns FROM Inserted
SELECT YourColumsn FROM Deleted
--Compare the values as you prefer
Thank you all for helping me.
In the morning when I stand up I got an Idea :)
The point was:
1. DECLARE AND Set Variables:
DECLARE #ai1 VARCHAR(30);
DECLARE #ad1 VARCHAR(30);
....
SET #ai1 = (SELECT a1 from inserted);
SET #ad1 = (SELECT a1 from deleted);
...
To Compare every #ai and #ad
IF NOT #ai1 = #ad1 OR (#ai1 IS NULL AND #ad1 IS NOT NULL) OR (#ai1 IS NOT NULL AND #ad1 IS NULL)
BEGIN
... Insert...
END
IF NOT #ai2 = #ad2...
Thats the Trick. But thank you all very much!!!

Check constraints in Ms Sql Sever 2005

I am trying to add a check constraint which verity if after an update the new value (which was inserted) is greater than old values which is already stored in table.
For example i have a "price" column which already stores value 100, if the update comes with 101 the is ok, if 99 comes then my constraint should reject the update process. Can this behavior be achieved using check constraints or should i try to use triggers or functions ?
Please advice me regarding this...
Thanks,
Mircea
Check constraints can't access the previous value of the column. You would need to use a trigger for this.
An example of such a trigger would be
CREATE TRIGGER DisallowPriceDecrease
ON Products
AFTER UPDATE
AS
IF NOT UPDATE(price)
RETURN
IF EXISTS(SELECT * FROM inserted i
JOIN deleted d
ON i.primarykey = d.primarykey
AND i.price< d.price)
BEGIN
ROLLBACK TRANSACTION
RAISERROR('Prices may not be decreased', 16, 1)
END
Triggers start as a quick fix, and end with a maintenance nightmare. The two big problems with triggers are:
It's hard to see when a trigger is called. You can easily write an update statement without being aware that a trigger will run.
When triggers start triggering other triggers, it becomes hard to tell what will happen.
As an alternative, wrap access to the table in a stored procedure. For example:
create table TestTable (productId int, price numeric(6,2))
insert into TestTable (productId, price) values (1,5.0)
go
create procedure dbo.IncreasePrice(
#productId int,
#newPrice numeric(6,2))
with execute as owner
as
begin
update dbo.TestTable
set price = #newPrice
where productId = #productId
and price <= #newPrice
return ##ROWCOUNT
end
go
Now if you try to decrease the price, the procedure will fail and return 0:
exec IncreasePrice 1, 4.0
select * from TestTable --> 1, 5.00
exec IncreasePrice 1, 6.0
select * from TestTable --> 1, 6.00
Stored procedures are pretty easy to read. Compared to triggers, they'll cause you a lot less headaches. You can enforce the use of stored procedures by not giving any user the right to UPDATE tables. That's a good practice anyway.

Resources