Reading Deleted or Update Value from the Open Transaction - tsql

I trying to open the transaction and then delete one record now i need to insert the deleted record into event table. The problem is i can't see the result because it has been deleted.
CREATE procedure [dbo].[TestData] ( #clientid bigint ) As
Begin
print ''abc''
insert into Client_Event_Log values ( getdate(),0,#clientid,100,''B0AE3162-671C-E211-AF2A-00155D051024'',NULL)
BEGIN TRY
BEGIN TRAN
Delete from access_types -- There is only record in the table.
8559230 abc 101 0 2010-01-01 10:25:25.000
select * from access_types -- cann't see the deleted record even before the session.
DECLARE #cGTAEventLog bigint
select #cGTAEventLog=Access_Type_Id from access_types
    
exec TestData #cGTAEventLog -- Now i am passing 8559230 to the SP to insert into event
table but it has been delete before so can't insert NULL
Commit Tran
END TRY
BEGIN CATCH
ROLLBACK TRAN
--Error message
PRINT 'Error: ' +
CONVERT(VARCHAR,ERROR_NUMBER()) + ' - ' +
CONVERT(VARCHAR,ERROR_SEVERITY()) + ' - ' +
CONVERT(VARCHAR,ERROR_STATE()) + ' - ' +
ERROR_MESSAGE() +
' Raise Error occurred at line ' + CONVERT(VARCHAR,ERROR_LINE())
END CATCH
END
I need to find a way to access the data after deleting so i can insert into a event table.

Assuming you are on SQL Server 2005 or later, you can use the output clause to access the virtual 'deleted' table and park its contents in a table variable. Something like the following
DECLARE #TmpTable TABLE (ID INT, ......)
DELETE
FROM access_types
OUTPUT Deleted.ID,... INTO #TmpTable
select * from #TmpTable

Related

Raise error without rollback in plpgsql/postgresql

I have two stored functions. delete_item which deletes one item, logging it's success or failure in an actionlog table, it returns a 1 on errors and 0 when running successfully.
Secondly I have another function remove_expired that finds what to delete and loops through it calling delete_item.
All this is intended to be called using a simple bash script (hard requirement from operations, so calling like this is not up for discussion), and it have to give an error code when things doesn't work for their reporting tools.
We want all the deletions possible to succeed (we don't expect errors, but humans are still humans, and errors does happen), so if we want to delete 10 items and 1 fails, we still want the other 9 to be deleted.
Secondly we would really like the logs to be in the table actionlog both in the success and error case. I.e., we want that log to be complete.
Since plpgsql functions doesn't allow manual transaction management that seems to not be an option (unless I missed a way to circumvent this?).
The only way I've found so far to achieve this is to wrap scripts around it outside plpgsql, but we would very much like this to be possible in pure plpgsql so we can just give operations a pssql -C ... command and then they shouldn't be concerned with anything else.
SQL to reproduce the problem:
DROP FUNCTION IF EXISTS remove_expired(timestamp with time zone);
DROP FUNCTION IF EXISTS delete_item(integer);
DROP TABLE IF EXISTS actionlog;
DROP TABLE IF EXISTS evil;
DROP TABLE IF EXISTS test;
CREATE TABLE test (
id serial primary key not null,
t timestamp with time zone not null
);
CREATE TABLE evil (
test_id integer not null references test(id)
);
CREATE TABLE actionlog (
eventTime timestamp with time zone not null default now(),
message text not null
);
INSERT INTO test (actualTime, t)
VALUES ('2020-04-01T10:00:00+0200'),
('2020-04-01T10:15:00+0200'), -- Will not be deleable due to foreign key
('2020-04-01T10:30:00+0200')
;
INSERT INTO evil (test_id) SELECT id FROM test WHERE id = 2;
CREATE OR REPLACE FUNCTION remove_expired(timestamp with time zone)
RETURNS void
AS
$$
DECLARE
test_id int;
failure_count int = 0;
BEGIN
FOR test_id IN
SELECT id FROM test WHERE t < $1
LOOP
failure_count := delete_item(test_id) + failure_count;
END LOOP;
IF failure_count > 0 THEN
-- I want this to cause 'psql ... -c "SELECT * FROM remove_expred...' to exit with exit code != 0
RAISE 'There was one or more errors deleting. See the log for details';
END IF;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION delete_item(integer)
RETURNS integer
AS
$$
BEGIN
DELETE FROM test WHERE id = $1;
INSERT INTO actionlog (message)
VALUES ('Deleted with ID: ' || $1);
RETURN 0;
EXCEPTION WHEN OTHERS THEN
INSERT INTO actionlog (message)
VALUES ('Error deleting ID: ' || $1 || '. The error was: ' || SQLERRM);
RETURN 1;
END
$$ LANGUAGE plpgsql;
Thanks in advance for any useful input
You can have something close to what your expect in PostgreSQL 11 or PostgreSQL 12 but only with procedures because as already said functions will always roll everything back in case of errors.
With:
DROP PROCEDURE IF EXISTS remove_expired(timestamp with time zone);
DROP PROCEDURE IF EXISTS delete_item(integer);
DROP FUNCTION IF EXISTS f_removed_expired;
DROP SEQUENCE IF EXISTS failure_count_seq;
DROP TABLE IF EXISTS actionlog;
DROP TABLE IF EXISTS evil;
DROP TABLE IF EXISTS test;
CREATE TABLE test (
id serial primary key not null,
t timestamp with time zone not null
);
CREATE TABLE evil (
test_id integer not null references test(id)
);
CREATE TABLE actionlog (
eventTime timestamp with time zone not null default now(),
message text not null
);
INSERT INTO test (t)
VALUES ('2020-04-01T10:00:00+0200'),
('2020-04-01T10:15:00+0200'), -- Will not be removed due to foreign key
('2020-04-01T10:30:00+0200')
;
select * from test where t < current_timestamp;
INSERT INTO evil (test_id) SELECT id FROM test WHERE id = 2;
CREATE SEQUENCE failure_count_seq MINVALUE 0;
SELECT SETVAL('failure_count_seq', 0, true);
CREATE OR REPLACE PROCEDURE remove_expired(timestamp with time zone)
AS
$$
DECLARE
test_id int;
failure_count int = 0;
return_code int;
BEGIN
FOR test_id IN
SELECT id FROM test WHERE t < $1
LOOP
call delete_item(test_id);
COMMIT;
END LOOP;
SELECT currval('failure_count_seq') INTO failure_count;
IF failure_count > 0 THEN
-- I want this to cause 'psql ... -c "SELECT * FROM remove_expred...' to exit with exit code != 0
RAISE 'There was one or more errors deleting. See the log for details';
END IF;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE PROCEDURE delete_item(in integer)
AS
$$
DECLARE
forget_value int;
BEGIN
DELETE FROM test WHERE id = $1;
INSERT INTO actionlog (message)
VALUES ('Deleted with ID: ' || $1);
EXCEPTION WHEN OTHERS THEN
INSERT INTO actionlog (message)
VALUES ('Error deleting ID: ' || $1 || '. The error was: ' || SQLERRM);
COMMIT;
SELECT NEXTVAL('failure_count_seq') INTO forget_value;
END
$$ LANGUAGE plpgsql;
--
I get:
select * from test;
id | t
----+------------------------
1 | 2020-04-01 10:00:00+02
2 | 2020-04-01 10:15:00+02
3 | 2020-04-01 10:30:00+02
(3 rows)
select current_timestamp;
current_timestamp
-------------------------------
2020-04-01 16:52:26.171975+02
(1 row)
call remove_expired(current_timestamp);
psql:test.sql:80: ERROR: There was one or more errors deleting. See the log for details
CONTEXT: PL/pgSQL function remove_expired(timestamp with time zone) line 17 at RAISE
select currval('failure_count_seq');
currval
---------
1
(1 row)
select * from test;
id | t
----+------------------------
2 | 2020-04-01 10:15:00+02
(1 row)
select * from actionlog;
eventtime | message
-------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------
2020-04-01 16:52:26.172173+02 | Deleted with ID: 1
2020-04-01 16:52:26.179794+02 | Error deleting ID: 2. The error was: update or delete on table "test" violates foreign key constraint "evil_test_id_fkey" on table "evil"
2020-04-01 16:52:26.196503+02 | Deleted with ID: 3
(3 rows)
I use a sequence to record the number of failures: you can use this sequence to test for failures and return the right return code.

Ingres stored procedure to delete multiple records from table

I am creating a SP in Ingres to delete multiple records from single table by comma separated id's, but it is not working. Though when I execute it in a separate query (without storedprocedure) then it is deleting records.
create procedure sptest
(
In Ids varchar(300)
)
AS
BEGIN
Delete from "ingres".mytable where request_id IN (:Ids);
END
Requested rows should be deleted from table
The input is a varchar so in effect what you have in the delete statement is something like:
delete from mytable where request_id in ('1,2,3,4');
Inside a database procedure you can't run "execute immediate", so you can't build a delete string without the quotes and execute it dynamically (though this might be an option to you if your calling program has "execute immediate" available).
To process the IN list within a database procedure I think you'll need to loop through the input string and delete for each value...
eg:
set session authorization ingres;
drop table if exists mytable;
create table mytable(request_id integer);
insert into mytable values(1),(2),(5),(10);
drop procedure if exists sptest;
create procedure sptest
(
In Ids varchar(300)
)
AS
declare msg = varchar(300) not null;
eno = integer not null;
rc = integer not null;
pos = integer not null;
n = varchar(300);
BEGIN
while (length(:Ids) > 0)
do
pos = locate(:Ids, ',');
n = left(:Ids, :pos-1);
Ids = shift(:Ids, -1 * pos);
msg = 'Removing ' + :n;
message :msg;
Delete from "ingres".mytable where request_id = integer(:n);
select iierrornumber, iirowcount into :eno, :rc;
msg = 'Error number '+varchar(:eno) + ' rowcount ' + varchar(:rc);
message :msg;
endwhile;
END;
execute procedure sptest('1,5,10');
select * from mytable;

Need help in creating a stored procedure to iterate tables in a database, then run a SQL statement on each table

Our application does not delete data as we retain it for a period of time, instead we have a column "deleted" (bit) in most tables of the database that store data which we mark 1 when deleted, otherwise the default is 0.
I'd like to create a stored procedure that iterates all tables in the database, checks for the existence of a column named "deleted" and if it exists, I run a check against the LastUpdatedUtc column (datetime2) and if the date is over 6 months old and deleted = 1 then we delete the row.
This application is under continuous development so tables could be added which is why I want to create a script that iterates tables instead of having to add a line for each table and remember to add them as new tables are added.
Any help in a SQL Server 2008 R2 stored procedure to this would be a great help.
Thank you.
EDIT (thank you Omaer) here is what I've come up with so far. Anyone that knows a better way let me know.
IF OBJECT_ID('tempdb..#tmpTables') IS NOT NULL DROP TABLE #tmpTables
GO
CREATE TABLE #tmpTables
(
ID INT,
TableName NVARCHAR(100) NOT NULL
)
GO
SET NOCOUNT ON
GO
INSERT #tmpTables
SELECT [object_id], [name] FROM sys.all_objects WHERE type_desc = 'USER_TABLE' ORDER BY [name]
DECLARE #TN NVARCHAR(100)
DECLARE #SQL NVARCHAR(max)
DECLARE #PurgeDate VARCHAR(50)
SET #PurgeDate = DATEADD(MONTH, -6, GETUTCDATE())
WHILE (SELECT COUNT(*) FROM #tmpTables) > 0
BEGIN
SELECT TOP 1 #TN = TableName FROM #tmpTables
IF EXISTS(SELECT * FROM sys.columns WHERE name = 'deleted' AND OBJECT_ID = OBJECT_ID(#TN))
BEGIN
IF EXISTS(SELECT * FROM sys.columns WHERE name = 'LastUpdatedUtc' AND OBJECT_ID = OBJECT_ID(#TN))
BEGIN
SET #SQL = 'SELECT Count(*) As Counter FROM ' + #TN + ' WHERE [deleted] = 1 AND [LastUpdatedUtc] < ''' + #PurgeDate + '''' -- this will be the delete line when the code is final, just outputting results for now
EXEC(#SQL)
END
END
DELETE #tmpTables WHERE TableName=#TN
END
DROP TABLE #tmpTables
This is my first attempt, not tested it so there might be some typos/syntax errors but this should get you started:
declare #date6MonthsBack varchar(50)
select #date6MonthsBack = dateadd(month, -6, getdate());
declare c cursor for
select 'delete from ' + quotename(name) + ' where [deleted] = 1 and [LastUpdatedUtc] <= ''' + #date6MonthsBack + '''' from sys.tables
where object_id in (select object_id from sys.columns where name = 'deleted')
and object_id in (select object_id from sys.columns where name = 'LastUpdatedUtc')
declare #sql varchar(max)
open c; fetch next from c into #sql
while (##fetch_status = 0) begin
print(#sql)
--exec(#sql) --uncomment this line to do the actual deleting once you have verified the commands.
fetch next from c into #sql; end
close c; deallocate c
You could use undocummented sp_MSforeactable procedure instead of loop or cursor. Something like code below. I created procedure that runs your code and is executed with sp_MSforeachtable. The disadvantage is - the procedure is undocumented and may not be supported in next SQL Server releases
IF OBJECT_ID('dbo.usp_cleanup') IS NULL
EXEC ('CREATE PROCEDURE dbo.usp_cleanup AS SELECT 1')
GO
ALTER PROCEDURE dbo.usp_cleanup
#sTblName VARCHAR(200)
AS
BEGIN
-- your variables
DECLARE #PurgeDate VARCHAR(50)
DECLARE #SQL VARCHAR(MAX)
SET #PurgeDate = DATEADD(MONTH, -6, GETUTCDATE())
-- we can check columns existence in one condition
IF
EXISTS(SELECT * FROM sys.columns WHERE name = 'deleted' AND OBJECT_ID = OBJECT_ID(#sTblName))
AND EXISTS(SELECT * FROM sys.columns WHERE name = 'LastUpdatedUtc' AND OBJECT_ID = OBJECT_ID(#sTblName))
BEGIN
SET #SQL = 'SQL CODE GOES HERE' -- this will be the delete line when the code is final, just outputting results for now
PRINT #SQL
--EXEC(#SQL) -- uncomment for execution
END
ELSE
-- for debugging
BEGIN
PRINT #sTblName + ' has no [delete] and [LastUpdatedUtc] columns'
END
END
EXEC sp_MSforeachtable 'exec usp_cleanup ''?'''
GO

Script design many-to-many insertion with partial rollback

I am trying to understand partial roll backs, in TSQL. Following a tutorial i found this example but I do not understand / find can you save to a table the second object if the first fails?
BEGIN TRAN
PRINT 'First Transaction: ' + CONVERT(VARCHAR,##TRANCOUNT)
INSERT INTO People VALUES ('Tom')
BEGIN TRAN
PRINT 'Second Transaction: ' + CONVERT(VARCHAR,##TRANCOUNT)
INSERT INTO People VALUES ('Dick')
ROLLBACK TRAN
PRINT 'Rollback: ' + CONVERT(VARCHAR,##TRANCOUNT)
So if 'Tom' fails can I save 'Dick' ?
I need this, for achieving a many-to-many roll back transaction. So i create Obj A, Obj B and if both succes add Obj C, if not roll back failure, and keep the successing INSERT query.
It looks like you just want to commit the transactions as they succeed. The code below keeps Tom, even if your script chokes on Dick...
CREATE TABLE #People
(
VAL VARCHAR(100)
);
DECLARE #stop BIT = 0;
BEGIN TRY
BEGIN TRAN
PRINT 'First Transaction: ' + CONVERT(VARCHAR,##TRANCOUNT)
INSERT INTO #People VALUES ('Tom')
COMMIT
END TRY
BEGIN CATCH
ROLLBACK TRAN;
SET #stop = 1;
END CATCH;
SELECT * FROM #People;
IF #stop = 0
BEGIN TRY
BEGIN TRAN
PRINT 'Second Transaction: ' + CONVERT(VARCHAR,##TRANCOUNT)
INSERT INTO #People VALUES ('Dick');
SELECT * FROM #People;
RAISERROR(
'Choke on Dick' -- stop snickering
, 16
, 1
);
COMMIT;
END TRY
BEGIN CATCH
PRINT 'Rollback: ' + CONVERT(VARCHAR,##TRANCOUNT)
ROLLBACK TRAN
END CATCH
SELECT * FROM #People;
DROP TABLE #People;

Reading inserted column names and values in a TSQL trigger

I've been asked to create history tables for every table in a database. Then create a trigger that will write to the history table whenever the primary table is updated.
The history tables have the same structure as the primary table, but with a couple of extra rows ('id' and 'update type')
I've never done anything with triggers before, but I would like to do is dynamically go through the columns in 'Inserted' and construct an insert statement to populate the history table.
However I cannot work out how to read the names of the columns and their individual values.
My half finished trigger currently looks like...
CREATE TRIGGER tr_address_history
ON address
FOR UPDATE
AS
DECLARE #colCount int
DECLARE #maxCols int
SET #colCount = 0
SET #maxCols = (SELECT COUNT(column_name) FROM INFORMATION_SCHEMA.columns WHERE TABLE_NAME = 'Inserted')
PRINT 'Number of columns = ' + CONVERT(varChar(10),#maxCols)
WHILE (#colCount <= #maxCols)
BEGIN
DECLARE #name varchar(255)
SELECT #name = column_name FROM INFORMATION_SCHEMA.columns WHERE TABLE_NAME = 'Inserted'
DECLARE #value varchar(255)
SELECT #value = #name FROM Inserted
PRINT 'name = ' + #name + ' and value = ' + #value
SET #colCount = #colCount + 1
END
PRINT 'Done';
When the trigger runs it just says "Number of columns = 0"
Can anyone tell me what's wrong with :
SELECT COUNT(column_name) FROM INFORMATION_SCHEMA.columns WHERE TABLE_NAME = 'Inserted'
Thanks...
First solution proposed by Beenay25 is good, but you should use affected table instead of 'inserted' pseudotable.
This is:
SELECT #name = column_name FROM INFORMATION_SCHEMA.columns WHERE TABLE_NAME = 'AFFECTED_TABLE'
Instead of 'INSERTED'
Also, you should use dynamic SQL.
This will be a complete working solution:
ALTER TRIGGER [dbo].[tr_address_history]
ON [dbo].[address]
AFTER Insert
AS
DECLARE #ColumnName nvarchar(500)
DECLARE #TableName nvarchar(500)
DECLARE #value nvarchar(500)
DECLARE #Sql nvarchar(500)
Set #TableName='address'
DECLARE ColumnsCursor CURSOR FOR
select column_name FROM INFORMATION_SCHEMA.columns WHERE TABLE_NAME = 'address'
OPEN ColumnsCursor
FETCH NEXT FROM ColumnsCursor into #ColumnName
WHILE ##FETCH_STATUS=0
BEGIN
select * into #tmp from inserted
Set #Sql= 'SELECT #value =' + #ColumnName + ' FROM #tmp'
EXEC sp_executesql #Sql, N'#Value nvarchar(500) OUTPUT', #Value OUTPUT
DROP TABLE #TMP
print '[' + #ColumnName +'='+ ltrim(rtrim(#Value))+']'
FETCH NEXT FROM ColumnsCursor into #ColumnName
END
CLOSE ColumnsCursor
DEALLOCATE ColumnsCursor
The 'inserted' table is a pseudo-table; it doesn't appear in INFORMATION_SCHEMA.
There is the UPDATE() operator for use in triggers:
CREATE TRIGGER trigger_name ON tablename
FOR UPDATE
AS
SET NOCOUNT ON
IF (UPDATE(Column1) OR UPDATE(Column2))
BEGIN
your sql here
END
COLUMNS_UPDATED
UPDATE()
There is a way to do what the questioner requires:
I have made something inside a trigger that tests whether all the columns of a particular table actually participated in an insert to that table. If they did, I later copied them to a history table. If they did not, then rollback and print only complete rows may be inserted into the report table. Perhaps they could adapt this to their needs:
here it is:
[
if exists (select 1 from inserted) and not exists (select 1 from deleted) -- if an insert has been performed
begin -- and we want to test whether all the columns in the report table were included in the insert
declare #inserted_columncount int, #actual_num_of_columns int, #loop_columns int, #current_columnname nvarchar(300),
#sql_test nvarchar(max), #params nvarchar(max), #is_there bit
set #actual_num_of_columns = (
select count(*) from (
select COLUMN_NAME
from INFORMATION_SCHEMA.COLUMNS
where TABLE_NAME = 'renameFilesFromTable_report') as z)
set #inserted_columncount = 0
set #loop_columns = 1
declare inserted_columnnames cursor scroll for -- these are not really the inserted ones, but we are going to test them 1 by 1
select COLUMN_NAME
from INFORMATION_SCHEMA.COLUMNS
where TABLE_NAME = 'renameFilesFromTable_report'
set #params = '#is_there_in bit output'
open inserted_columnnames
fetch next from inserted_columnnames into #current_columnname
select * into #temp_for_dynamic_sql from inserted -- this is necessary because the scope of sp_executesql does not include inserted pseudo table
while (#loop_columns <= #actual_num_of_columns) -- looping with independent integer arithmetic
begin
set #sql_test = '
set #is_there_in = 0
if exists (select ['+#current_columnname+'] from #temp_for_dynamic_sql where ['+#current_columnname+'] is not null)
set #is_there_in = 1'
exec sp_executesql #sql_test, #params, #is_there output
if #is_there = 1
begin
fetch next from inserted_columnnames into #current_columnname
set #inserted_columncount = #inserted_columncount + 1
set #loop_columns = #loop_columns + 1
end
else if #is_there <> 1
begin
fetch next from inserted_columnnames into #current_columnname
set #loop_columns = #loop_columns + 1
end
end
close inserted_columnnames
deallocate inserted_columnnames
-- at this point we hold in two int variables the number of columns participating in the insert and the total number of columns
]
Then you can simply do if #inserted_columncount < #actual_num_of_columns ..........
I did this because i have a sp that inserts 1 complete line to the report table every time it runs. That's fine, but i don't want anyone else touching that table by mistake. not even myself. I also want to keep history. So i made this trigger to keep the history but also to check if an insert was attempted without values for all the columns in the report table, and further down the code it checks if an update or delete was attempted and it rollbacks.
i was thinking of expanding this to allow an update but in which all the columns are set.
this could possibly be done as follows:
if update was attempted,
and exists (
select possibly_excluded.COLUMN_NAME from (
select COLUMN_NAME
from INFORMATION_SCHEMA.COLUMNS
where TABLE_NAME = 'renameFilesFromTable_report') as possibly_excluded
group by possibly_excluded.COLUMN_NAME
having COLUMN_NAME not in (
select COLUMN_NAME
from INFORMATION_SCHEMA.COLUMNS
where TABLE_NAME = 'renameFilesFromTable_report' and
sys.fn_IsBitSetInBitmask(#ColumnsUpdated, COLUMNPROPERTY(OBJECT_ID(TABLE_SCHEMA + '.' + TABLE_NAME), COLUMN_NAME, 'ColumnID')) <> 0)
)
begin
rollback transaction
print 'Only updates that set the values for a complete row are allowed on the report table..'
end