Avoid recursion in a UPDATE trigger with dynamic SQL - sql-server

We are building a protected INSTEAD OF UPDATE trigger to monitor and control the updates of several tables (most of MasterData tables of the system, about 150 of them). So, as much as possible (installation, updates) we try to do the code as reusable as possible (no hard-coding field names or table names).
To control the "actual" version of a row, an _ACTIVE field exist, and this goes decreasing for each new version (ACTIVE row gets ACTIVE = 1). Sorry we can not use the temporal tables feature due to backwards compatibility (plenty of business logic is built based on this feature)
Update logic includes treating the OLD and NEW lines before they affect the table, and once everything is treated, update the table; not only the affected rows, but all with same uniqueness key fields (the identification of the uniqueness fields aimed to be done dynamically too; on the following example, the where clause gets dynamically constructed on the variable #toWhereOnClause)
The "real" table suffers two actions, first a bunch of new lines are inserted with _ACTIVE = 2, second, all rows that need to be update get the _ACTIVE -= 1, leaving the newest version of the row set to 1
The problem arise as this second action, the update, needs to be created dynamically, to avoid entering the table name, and set the #toWhereOnClause manually. And this triggers once more the TRIGGER, and because it is dynamicSQL (we believe) is not captured by the TRIGGER_NESTLEVEL() = 1
Code structure is as follow:
CREATE OR ALTER TRIGGER [schema].[triggerName] ON [schema].table
INSTEAD OF UPDATE
AS
BEGIN
SET #tableName = '[schema].[table]' // only line to modify for diferent tables
//TRIGGER PREPARATION
SET #schema = (SELECT SUBSTRING(#tableName, 1, (CHARINDEX('].', #tableName))))
SET #table = (SELECT SUBSTRING(#tableName, (CHARINDEX('[', #tableName, 3)), LEN(#tableName)))
SET #fieldNameS = (SELECT + ',' + QUOTENAME(COLUMN_NAME)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = #schema
AND TABLE_NAME = #table
ORDER BY ORDINAL_POSITION
FOR XML path(''));
SET #uniqueFieldSelector = (SELECT +' AND LeftTable.'+ COLUMN_NAME + ' = #INSERTED.' + COLUMN_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE TABLE_NAME = #tableName
AND COLUMN_NAME NOT LIKE '%Active%'
FOR XML PATH(''))
SET #toWhereOnClause = (SELECT (SUBSTRING(#uniqueFieldSelector, 5, LEN(#uniqueFieldSelector))))
// DUPLICATE TRIGGER TABLES INTO TEMP TABLE TO WORK ON NEW AND OLD LINES
SELECT * INTO #INSERTED FROM INSERTED -- Can't modify logic table values (INSERTED), we put it in a temp table
SELECT * INTO #DELETED FROM DELETED
// SEVERAL INSTRUCTIONS TO TREAT THE OLD AND NEW LINES (not shown here) AND CALCULATE IF THE UPDATE IS LEGAL (#CONTINUE_TRIGGER)
...
// REAL UPDATE
IF TRIGGER_NESTLEVEL() = 1 AND #CONTINUE_TRIGGER = TRUE
--https://stackoverflow.com/questions/1529412/how-do-i-prevent-a-database-trigger-from-recursing
BEGIN
SET #statementINSERT = N'INSERT INTO' + #tableName + '( ... )
SELECT ... FROM #INSERTED ';
EXECUTE sp_executesql #statementINSERT
SET #statementUPDATE = N'UPDATE TheRealTable
SET TheRealTable._ACTIVE -= 1
FROM ' + #tableName + ' AS TheRealTable
INNER JOIN #INSERTED ON ' + #toWhereOnClause;
EXECUTE sp_executesql #statementUPDATE
END
END
yes, we know it is complex, but legacy doesn't give many options.
SO:
Is there any way to avoid the dynamicSQL trigger again the TRIGGER ??
(system is running on WindowsServer, and Azure instances, all with 120 compatibility at least)

Related

Create DELETE query by SELECTING from table

in my project (automation-testing) I am adding a lot of data into my Database, which supposed to be deleted after running all scenarios, but in rare occasions when execution crashes I am sometimes left with records that were not deleted.
For this reason, I decided in addition store all records that were created into additional table, where I store:
new of the table where new record was inserted
name of the field used for where clause in delete statement
id of the record in its table
Now I am trying to select all records from table above, and use it to create delete queries.
Is it possible to do it in single query?
SELECT * FROM AutomationTestingData AS atd
DELETE FROM atd.TableName WHERE atd.DeleteByField = atd.RecordId
Any help would be appreciated, regards.
Hi if i understand all what you're trying i think this can respond :
SET NOCOUNT ON;
DECLARE #query varchar(4000);
PRINT '-------- Deleting rows --------';
DECLARE deleting_cursor CURSOR FOR
SELECT 'DELETE FROM ' + atd.TableName + ' WHERE ' + atd.DeleteByField + ' = ' + atd.RecordId + ';'
FROM AutomationTestingData AS atd
OPEN deleting_cursor
FETCH NEXT FROM deleting_cursor
INTO #query
WHILE ##FETCH_STATUS = 0
BEGIN
EXEC(#Query)
FETCH NEXT FROM deleting_cursor
INTO #query
END
CLOSE deleting_cursor;
DEALLOCATE deleting_cursor;
CURSOR SQL : https://learn.microsoft.com/en-us/sql/t-sql/language-elements/declare-cursor-transact-sql?view=sql-server-2017
EXECUTE dynamic SQL Server : https://learn.microsoft.com/en-us/sql/t-sql/language-elements/execute-transact-sql?view=sql-server-2017
You could write a delete statement for each table, like this
delete from Company
where id in ( select RecordId from adt where adt.TableName = 'Company')
delete from PickListValues
where id in ( select RecordId from adt where adt.TableName = 'PickListValues')
Granted, its not really dynamic, but its still faster then dynamic sql and a cursor
Edit
IF you need it fully automatic, use dynamic sql like suggested by #pascalsanchez
I would alter the cursor however like this
declare deleting_cursor cursor for
select 'delete from ' + adt.TableName +
'where ' + adt.DeleteByField + ' in (select RecordID from adt where adt.TableName = ''' + adt.TableName + ''')
from AutomationTestingData AS atd
group by adt.TableName, adt.DeleteByField
you can use this syntax :
delete T1
from Table1 T1 join Table2.T2 on T1.SomeField=T2.SomeField
where Some Condition

Fast Update of thousands of rows in SQL database

I would like to update one column value for all 19k rows I would like to know the fastest way of updating thousands of rows in a SQL database. Please suggest.
Below is the code I tried, but it's taking full day to execute which making the existing application freeze.
update table_name
set column_name_value = 2
If you are simply changing the whole column to a single value, I would ask what is the point of having the column in the first place. Either way, If you're updating a LIVE production transactional database, I'd take a backup first.
If you are allowed, switch the database into SIMPLE mode. This will reduce the amount of space the trans log uses, or try BULK INSERT mode. This may not be an option.
That said, you can drop the column, and readd it with a default.
ALTER TABLE table_name DROP COLUMN column_name;
ALTER TABLE table_name ADD column_name INTEGER NOT NULL DEFAULT 2;
You may also do something like this:
UPDATE table_name WITH (TABLOCKX) SET column_name = 2;
If you cannot do any of the preceding, taking the database out of commission, then you may have to chunk your updates.
Try the following (if you happen to have a identity key on the table):
DECLARE #num_chunks INT = 10 -- this will break your update into 10 smaller updates
DECLARE #counter INT = 0
WHILE #counter < #num_chunks
BEGIN
UPDATE table_name SET column_name = 2
WHERE (int_identity_column - #counter) % #num_chunks = 0
PRINT 'DONE WITH GROUP ' + CAST(#counter AS VARCHAR) + ' AT ' + CAST(getdate() as varchar)
SET #counter = #counter + 1
END
Using this method will allow you to restart the process from where it left off, if you happen to have an issue and it gets interrupted.

The database generated a key that is already in use, caused by Trigger?

I have a legacy production software that uses LINQ to SQL, and it's own database. I wanted to create a trigger on one of the tables in that database and have it do a few joins and keep a field in another database current with its values. What has happened now is with the trigger I get this error in my legacy application:
"The database generated a key that is already in use."
If I remove the trigger, everthing works as per usual.
Here is the trigger:
BEGIN
SET NOCOUNT ON;
DECLARE #action as char(1);
if exists(SELECT * from inserted) and exists (SELECT * from deleted)
begin
SET #action = 'UPDATE';
INSERT INTO ste.dbo.Logs([LogEvent],[RaisedBy],[LogTime])
SELECT
itemID,
'ListLineItem (' + ListLineItemID + '): ' + #action,
GETDATE()
FROM INSERTED
end
If exists (Select * from inserted) and not exists(Select * from deleted)
begin
SET #action = 'INSERT';
update soli
set soli.itemRefNumber = wo.RefNum,
soli.itemIssueDate = wo.IssueDate
from ste.dbo.ListLineItems soli
join INSERTED woli on soli.ListLineTxnID = woli.ListLineItemID
join cse.dbo.item wo on woli.itemID = wo.IDKey
SELECT
itemID,
'ListLineItem (' + ListLineItemID + '): ' + #action,
GETDATE()
FROM INSERTED
end
If exists(select * from deleted) and not exists(Select * from inserted)
begin
SET #action = 'DELETE';
update soli
set soli.itemRefNumber = null,
soli.itemIssueDate = '19000101'
from ste.dbo.ListLineItems soli
join deleted woli on soli.ListLineTxnID = woli.ListLineItemID
join cse.dbo.item wo on woli.itemID = wo.IDKey
SELECT
itemID,
'ListLineItem (' + ListLineItemID + '): ' + #action,
GETDATE()
FROM deleted
end
END
Thoughts?
In place of auto generated primary key code should use guuid. As I think your application is running in multi threaded mode with a race condition between multiple thread leading to this issue.
Learn to post useful information. Why do you cut off the actual declaration of the trigger name, table, and type? You also cut off the last part of the trigger code. Next, post the complete text of all errors - not your interpretation. Finally, read all you can about triggers and problems the inexperienced find trying to write them.
Your problems? In your insert and delete blocks, you attempt to return a resultset - DON'T. Did you copy/paste incorrectly? And what did you intend when you defined #action as a single character string? You don't really use it as a variable so why add the complexity of assigning a multi-character string to it when a literal can be used?
Lastly, the error message you posted sounds like it was generated by your application. The most likely explanation is that the app "sees" the resultset of your trigger and misinterprets the information.
And one last comment - what happens when a merge statement statement causes the trigger to execute? Will it work correctly and record the appropriate information? Be careful what you assume and how you test.

How to return local temporary table from generated sql

I have filtering SQL that returns query with uncertain number of columns, and want to use results in stored procedure.
DECLARE #RecordSelectionSql VARCHAR(MAX)
SET #RecordSelectionSql = (SELECT SQLQUERY FROM RecordSelection WHERE Id = #Id) + ' AND ([Active] = 1)'
DECLARE #sql NVARCHAR(MAX) = N'';
SELECT #sql += ',' + CHAR(13) + CHAR(10) + CHAR(9) + name + ' ' + system_type_name
FROM sys.dm_exec_describe_first_result_set(#RecordSelectionSql, NULL, 0);
SELECT #sql = N'CREATE TABLE #TmpImport
(' + STUFF(#sql, 1, 1, N'') + '
);';
EXEC (#sql)
INSERT INTO #TmpImport
EXEC (#RecordSelectionSql)
However I am getting error
Invalid object name '#TmpImport'.
How to properly code this part?
EDIT: added missing condition on RecordSelection
EDIT2:
I cannot use code below because #TmpImport destroyed after #RecordSelectionSql being executed.
DECLARE #RecordSelectionSql AS VARCHAR(MAX)
SET #RecordSelectionSql = 'SELECT X.* INTO #TmpImport FROM ('
+ (SELECT SQLQUERY FROM RecordSelection WHERE Id = #Id) + ' AND ([Active] = 1) AS X'
EXEC (#RecordSelectionSql)
SELECT * FROM #TmpImport
Gives the same error
Invalid object name '#TmpImport'.
Temporary tables are only available within the session that created them. With Dynamic SQL this means it is not available after the Dynamic SQL has run. Your options here are to:
Create a global temporary table, that will persist outside your session until it is explicitly dropped or cleared out of TempDB another way, using a double hash: create table ##GlobalTemp
--To incorporate Radu's very relevant comment below: Because this table persists outside your session, you need to make sure you don't create two of them or have two different processes trying to process data within it. You need to have a way of uniquely identifying the global temp table you want to be dealing with.
You can create a regular table and remember to drop it again afterwards.
Include whatever logic that needs to reference the temp table within the Dynamic SQL script
For your particular instance though, you are best off simply executing a select into which will generate your table structure from the data that is selected.
It's much easier to select into your temp table.
For example
SELECT * INTO #TmpImport FROM SomeTable

SQL Server Compare field value with its default

I need to iterate through the fields on a table and do something if its value does not equal its default value.
I'm in a trigger and so I know the table name. I then loop through each of
the fields using this loop:
select #field = 0, #maxfield = max(ORDINAL_POSITION) from
INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = #TableName
while #field < #maxfield
begin
...
I can then get the field name on each iteration through the loop:
select #fieldname = COLUMN_NAME from INFORMATION_SCHEMA.COLUMNS
where TABLE_NAME = #TableName
and ORDINAL_POSITION = #field
And I can get the default value for that column:
select #ColDefault = SUBSTRING(Column_Default,2,LEN(Column_Default)-2)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE Table_Name = #TableName
AND Column_name = #fieldname
I have everything I need but I can't see how to then compare the 2. Because
I don't have the field name as a constant, only in a variable, I can't see
how to get the value out of the 'inserted' table (remember I'm in a trigger)
in order to see if it is the same as the default value (held now in
#ColDefault as a varchar).
First, remember that a trigger can be fired with multiple records coming in simultaneously. If I do this:
INSERT INTO dbo.MyTableWithTrigger
SELECT * FROM dbo.MyOtherTable
then my trigger on the MyTableWithTrigger will need to handle more than one record. The "inserted" pseudotable will have more than just one record in it.
Having said that, to compare the data, you can run a select statement like this:
DECLARE #sqlToExec VARCHAR(8000)
SET #sqlToExec = 'SELECT * FROM INSERTED WHERE [' + #fieldname + '] <> ' + #ColDefault
EXEC(sqlToExec)
That will return all rows from the inserted pseudotable that don't match the defaults. It sounds like you want to DO something with those rows, so what you might want to do is create a temp table before you call that #sqlToExec string, and instead of just selecting the data, insert it into the temp table. Then you can use those rows to do whatever exception handling you need.
One catch - this T-SQL only works for numeric fields. You'll probably want to build separate handling for different types of fields. You might have varchars, numerics, blobs, etc., and you'll need different ways of comparing those.
I suspect you can do this using and exec.
But why not just code generate once. It will be more performant

Resources