I've got a Trigger (yes I know, yuk) that needs to check that a bunch of updates to another table has finished before sending out a notification.
I can't avoid having to do this in a Trigger like this due to the source database being 3rd party and being designed badly in my opinion.
The problem being is that the trigger has its own transaction and does not detect any changes to the table, as they are happening outside of it.
Is there any way to get around this?
In the code below, I'm getting values from the metricvalue table and creating xml with it. currently it doesn't get all the available records as the trigger is being called before all the metrics are written (there is no way to avoid this unfortunately). I was hoping to put in a .5 sec pause and then do the query, but because of the transaction problem this doesn't work.
code:
ALTER TRIGGER [dbo].[STR_MetricStatusChange]
ON [dbo].[MetricStatus]
FOR INSERT, UPDATE
AS
BEGIN
DECLARE #Runid UNIQUEIDENTIFIER;
DECLARE #run_number INT;
DECLARE #sample_number INT;
DECLARE #primary_step INT;
DECLARE #secondary_step int;
DECLARE #secondary_step_maths_done int;
DECLARE #old_secondary_step_maths_done int;
DECLARE #primary_step_maths_done int;
DECLARE #old_primary_step_maths_done int;
DECLARE #Message nvarchar(MAX);
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
-- Get the fields we need from the current MetricStatus record
--
SELECT #Runid = i.runid,
#run_number = r.Run_Number,
#sample_number = i.sample_number,
#primary_step = i.primary_step,
#secondary_step_maths_done = i.secondary_step_maths_done,
#primary_step_maths_done = i.primary_step_maths_done
FROM INSERTED i
LEFT JOIN dbo.Runs r ON r.Run_id = i.runid
-- Standard Metrics secondary step notification to Notification Queue
--
SELECT #Message = (SELECT (SELECT 0 AS MessageType FOR XML PATH (''), TYPE ),
(SELECT #Runid AS RunId, #run_number AS RunNumber, #sample_number AS SampleNumber, #primary_step AS PrimaryStep, #secondary_step AS SecondaryStep, GETDATE() AS [TimeStamp] FOR XML PATH('Details'), TYPE),
(SELECT
(SELECT mv.MetricValue_MetricId AS '#MetricId',
mv.MetricValue_Value AS Value
FROM dbo.MetricValues mv
WHERE MetricValue_RunId = #Runid and MetricValue_SampleNumber = #sample_number
for XML PATH('Metric'), TYPE)
FOR XML PATH('Metrics'),TYPE)
FOR XML PATH (''), ROOT('Notification'))
-- Standard Metrics secondary step notification to Maths Engine Queue
--
EXEC dbo.usp_SendNotification #Message, 'MathsEngine';
END
Related
I've developed the following script:
declare #query nvarchar(1000)
declare #Loop int
declare #Whse table
( ID int identity primary key
, WhseLink int)
insert into #Whse
( WhseLink )
select WhseLink from WhseMst
select #Loop = min(ID) from #Whse
while #Loop is not null
begin
set #query = 'exec _bspWhUtilAddAllStkToWh('+cast((select WhseLink from #Whse where ID = #Loop) as varchar)+')'
exec #query
select #Loop = min(ID) from #Whse where ID>#Loop
end
Based on the above, I get the following Error:
Could not find stored procedure 'exec _bspWhUtilAddAllStkToWh 2'
I've checked the following Link which the user also had the same problem, but I think this one is different to that, due to the fact that the Stored Procedure actually exists and when I run the same script separately, it works.
I've tried adding brackets so that the #query eventually looks like this : 'exec (_bspWhUtilAddAllStkToWh) 2', but I still receive the same error.
What am I missing?
Ideally you should avoid looping at all costs. In your situation I would consider changing your procedure to receive a table valued function so you can receive a whole collection of Warehouse Links and do whatever is in your procedure on the entire set. But assuming you can't do that you can use a cursor here and forget the dynamic sql because it isn't needed.
Something like this is a lot simpler.
declare #WarehouseLink int
declare Warehouses cursor local fast_forward
for
select WhseLink
from WhseMst
fetch next from Warehouses into #WarehouseLink
while ##FETCH_STATUS = 0
begin
exec _bspWhUtilAddAllStkToWh #WarehouseLink
fetch next from Warehouses into #WarehouseLink
end
I have a merge statement supposed to execute a trigger multiple times.
I first thought my trigger wasn't executing, but with some research I found that triggers are only triggered once per statement (a trigger being one statement).
But all the posts out there are old and I thought that there might be a simple way now to make my trigger execute multiple times.
So is there anything I can add to my trigger or my merge statement to make my trigger do so?
Thanks
TRIGGER
TRIGGER [dbo].[Sofi_TERA_Trigger]
ON [dbo].[ZZ]
AFTER INSERT,UPDATE
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
IF EXISTS(SELECT 1 FROM inserted WHERE inserted.Statut LIKE '%CLOT%' OR inserted.Statut LIKE '%CLTT%' OR inserted.Statut LIKE '%CONF%')
BEGIN
DECLARE #Id int;
DECLARE #Matricule varchar(10);
DECLARE #IdAction int;
DECLARE #NumeroOF int;
SELECT #NumeroOF = inserted.Ordre from inserted;
DECLARE OF_CURSOR CURSOR
LOCAL STATIC READ_ONLY FORWARD_ONLY
FOR
SELECT Id,Log.Matricule,IdAction from Log inner join (select max(Id) as maxID,Matricule from LOG where Log.NumeroOF = #NumeroOF group by Matricule) maxID
on maxID.maxID = Log.Id where Log.NumeroOF = #NumeroOF;
OPEN OF_CURSOR
FETCH NEXT FROM OF_CURSOR INTO #Id,#Matricule,#IdAction
WHILE ##FETCH_STATUS = 0
BEGIN
IF #IdAction!=13
BEGIN
IF #IdAction<=2
BEGIN
insert into Log(NumeroOF,Matricule,IdAction,Date,EstAdmin) values (#NumeroOF,#Matricule,13,GETDATE(),1);
END
ELSE
BEGIN
insert into Log(NumeroOF,Matricule,IdAction,Date,EstAdmin) values (#NumeroOF,#Matricule,2,GETDATE(),1);
insert into Log(NumeroOF,Matricule,IdAction,Date,EstAdmin) values (#NumeroOF,#Matricule,13,GETDATE(),1);
END
END
FETCH NEXT FROM OF_CURSOR INTO #Id,#Matricule,#IdAction
END
CLOSE OF_CURSOR;
DEALLOCATE OF_CURSOR;
END
END
MERGE STATEMENT
Merge ZZ AS TARGET USING ZZTemp AS SOURCE
ON (Target.Operation=Source.Operation AND Target.Ordre=Source.Ordre)
WHEN MATCHED THEN
UPDATE SET TARGET.DateTERA=SOURCE.DateTERA, TARGET.MatTERA=SOURCE.MatTERA, TARGET.MatTERC=SOURCE.MatTERC
WHEN NOT MATCHED THEN
INSERT(Operation,Ordre,ElementOTP,Article,DesignationOF,PosteTravail,ValeurTemps,DHT,Statut,StatutOF,TexteActivite,DateTERA,MatTERA,MatTERC,StatutMat)
VALUES(SOURCE.Operation,SOURCE.Ordre,SOURCE.ElementOTP,SOURCE.Article,SOURCE.DesignationOF,SOURCE.PosteTravail,SOURCE.ValeurTemps,SOURCE.DHT,
SOURCE.Statut,SOURCE.StatutOF,SOURCE.TexteActivite,SOURCE.DateTERA,SOURCE.MatTERA,SOURCE.MatTERC,SOURCE.StatutMat);
Your problem is the cursor is incorrectly written to handle sets of data. Any trigger setting a value form inserted or deleted to a scalar variable is incorrect and for reasons of data integrity MUST be rewritten. This trigger is buggy. Period. There is no getting around that it must be rewritten (and any others that use the same technique).
The code inside your trigger should be something like:
INSERT INTO Log(NumeroOF,Matricule,IdAction,Date,EstAdmin)
SELECT max(Id),l.Matricule,l.IdAction, 13,GETDATE(),1
FROM Log l
JOIN Inserted i ON l.NumeroOF = i.Ordre
WHERE i.Statut LIKE '%CLOT%' OR i.Statut LIKE '%CLTT%' OR i.Statut LIKE '%CONF%'
GROUP BY l.Matricule,l.IdAction
INSERT INTO Log(NumeroOF,Matricule,IdAction,Date,EstAdmin)
SELECT max(Id),l.Matricule,l.IdAction, 2,GETDATE(),1
FROM Log l
JOIN Inserted i ON l.NumeroOF = i.Ordre
WHERE IdAction<=2
WHERE i.Statut LIKE '%CLOT%' OR i.Statut LIKE '%CLTT%' OR i.Statut LIKE '%CONF%'
GROUP BY l.Matricule,l.IdAction
Make sure to test with both single record and multiple record inserts as all triggers should be tested. Then try your MERGE once you are confident the trigger is correct.
I'm trying to make a bit of SQL script that will output all the table names in a large database, along with the numbers of fields and records in each, and a list of the field names. This will allow us to focus on the tables with data, and look for field names that match from different tables, which might be appropriate places for joins.
To do this, I'm trying to write dynamic SQL that can cycle through all the tables. But I haven't been able to get sp_executesql to yield outputs that I can insert into my table variable. Here's the code I've written so far:
USE MITAS_TEST;
DECLARE #TablesAbstract TABLE(
TableName VARCHAR(50),
NumberOfFields INT,
NumberOfRecords INT
);
DECLARE #NumberOfRowsCounted INTEGER;
SET #NumberOfRowsCounted = 0;
DECLARE #RecSql NVARCHAR(500);
SET #RecSql = 'EXECUTE(''SELECT #NumberOfRows = COUNT(*) FROM ''+#TableName)';
DECLARE #ParmDefinition NVARCHAR(100);
SET #ParmDefinition = '#TableName NVARCHAR(100), #NumberOfRows INTEGER OUTPUT';
DECLARE #TableN NVARCHAR(100);
SET #TableN = 'MITAS_TEST.dbo.AP500';
EXECUTE sp_executesql #RecSql,
#ParmDefinition,
#TableName = #TableN,
#NumberOfRows = #NumberOfRowsCounted OUTPUT;
I get the following error:
Msg 137, Level 15, State 1, Line 1
Must declare the scalar variable "#NumberOfRows"
I would have thought it sufficed to declare #NumberOfRows in the #ParmDefinition field (based on this source: https://technet.microsoft.com/en-us/library/ms188001(v=sql.90).aspx). What am I doing wrong? Is there a better way?
The inner execute looks like:
EXECUTE('SELECT #NumberOfRows = COUNT(*) FROM MITA_TEST.dbo.AP500')
In that context, #NumberOfRows does not exist. You could change that to another call to sp_executesql, passing the output parameter #NumberOfRows down another level.
Assuming that this represents learning code, and not production. Depending on the source of #TableN, this could be susciptable to SQL injection attacks. See quotename in books online.
I have a stored procedure which does a lot of probing of the database to determine if some records should be updated
Each record (Order) has a TIMESTAMP called [RowVersion]
I store the candidate record ids and RowVersions in a temporary table called #Ids
DECLARE #Ids TABLE (id int, [RowVersion] Binary(8))
I get the count of candidates with the the following
DECLARE #FoundCount int
SELECT #FoundCount = COUNT(*) FROM #Ids
Since records may change from when i SELECT to when i eventually try to UPDATE, i need a way to check concurrency and ROLLBACK TRANSACTION if that check fails
What i have so far
BEGIN TRANSACTION
-- create new combinable order group
INSERT INTO CombinableOrders DEFAULT VALUES
-- update orders found into new group
UPDATE Orders
SET Orders.CombinableOrder_Id = SCOPE_IDENTITY()
FROM Orders AS Orders
INNER JOIN #Ids AS Ids
ON Orders.Id = Ids.Id
AND Orders.[RowVersion] = Ids.[RowVersion]
-- if the rows updated dosnt match the rows found, then there must be a concurrecy issue, roll back
IF (##ROWCOUNT != #FoundCount)
BEGIN
ROLLBACK TRANSACTION
set #Updated = -1
END
ELSE
COMMIT
From the above, i'm filtering the UPDATE with the stored [RowVersion] this will skip any records that have since been changed (hopefully)
However i'm not quite sure if i'm using transactions or optimistic concurrency in regards to TIMESTAMP correctly, or if there are better ways to achieve my desired goals
It's difficult to understand what logic you are trying to implement.
But, if you absolutely must perform several non-atomic actions in a procedure and make sure that the whole block of code is not executed again while it is running (for example, by another user), consider using sp_getapplock.
Places a lock on an application resource.
Your procedure may look similar to this:
CREATE PROCEDURE [dbo].[YourProcedure]
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
BEGIN TRANSACTION;
BEGIN TRY
DECLARE #VarLockResult int;
EXEC #VarLockResult = sp_getapplock
#Resource = 'UniqueStringFor_app_lock',
#LockMode = 'Exclusive',
#LockOwner = 'Transaction',
#LockTimeout = 60000,
#DbPrincipal = 'public';
IF #VarLockResult >= 0
BEGIN
-- Acquired the lock
-- perform your complex processing
-- populate table with IDs
-- update other tables using IDs
-- ...
END;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
END CATCH;
END
When you SELECT the data, try using HOLDLOCK and UPDLOCK while inside of an explicit transaction. It's going to mess with the concurrency of OTHER transactions but not yours.
http://msdn.microsoft.com/en-us/library/ms187373.aspx
For the application I work on... we're creating a custom logging system. The user can view logs and apply "Tags" to them (Just like how you can apply tags to questions here!)
In this example, I'm trying to get a list of all the Logs given a "Tag." I realize I can accomplish this by using joins... but this is also an exercise for me to learn Stored Procedures a little better :)
I have a stored procedure that looks something like this to select a log by the PK
ALTER PROCEDURE [dbo].[getLogByLogId]
-- Add the parameters for the stored procedure here
#ID int
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for procedure here
SELECT TOP 1
LOG_ID,
a.A,
a.B,
a.C
FROM dbo.LOG a
WHERE a.LOG_ID = #ID
Now I would like to call this Stored Procedure from another... something like this
ALTER PROCEDURE [dbo].[getLogsByTagName]
-- Add the parameters for the stored procedure here
#TAG nvarchar(50)
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for procedure here
SELECT TOP 1000
LOG_ID --somehow store this and execute the dbo.getLogByLogId procedure here
FROM dbo.LOG_TAG a
WHERE a.TAG = #TAG
Thanks
If you have complex logic in your logbyid SP which you are trying to avoid reproducing in multiple places in your system (choice of columns, derived columns, etc), I would recommend turning that into an inline table-valued function instead (potentially without taking the ID parameter, in which case, you can actually use an ordinary view).
Then you can either join to that ITVF/view in your other stored proc (or also make another udf) which does the search or use the OUTER APPLY functionality (not as efficient).
Inline table-valued functions are basically parameterized views and can be optimized fairly easily by the optimizer.
If you want to call another sproc from within a sproc just use:
CREATE PROCEDURE myTestProc
AS
BEGIN
--Do some work in this procedure
SELECT blah FROM foo
--now call another sproc
EXEC nameOfSecondSproc
END
The only way you can achive what you are attempting is by using a CURSOR.
If this is for your learning only, then by all means, give this a go, but I would not recomend this for production.
It would go something like this
DECLARE #Table TABLE(
ID INT
)
INSERT INTO #Table SELECT 1
INSERT INTO #Table SELECT 2
INSERT INTO #Table SELECT 3
INSERT INTO #Table SELECT 4
INSERT INTO #Table SELECT 5
INSERT INTO #Table SELECT 6
DECLARE Cur CURSOR FOR
SELECT ID
FROM #Table
OPEN Cur
DECLARE #ID INT
FETCH NEXT FROM Cur INTO #ID
WHILE ##FETCH_STATUS = 0
BEGIN
PRINT #ID
FETCH NEXT FROM Cur INTO #ID
END
CLOSE Cur
DEALLOCATE Cur
By using the #ID retrieved in the WHILE loop, you can then execute the sp you wish and insert the values into a table variable.
INSERT INTO #Table EXEC sp_MySP #ID
You can call a stored procedure from another using the following syntax:
ALTER PROCEDURE [dbo].[getLogsByTagName]
-- Add the parameters for the stored procedure here
#TAG nvarchar(50)
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for procedure here
SELECT TOP 1000
LOG_ID --somehow store this and execute the dbo.getLogByLogId procedure here
FROM dbo.LOG_TAG a
WHERE a.TAG = #TAG
-- Execute dbo.getLogByLogId stored procedure
DECLARE #logId INTEGER
SET #logId = <some value>
EXEC dbo.getLogByLogId #logId
END
However, the difficult part of your question is that your dbo.getLogByLogId procedure can only accept a single LogID parameter and therefore will only be able to return a single Log record. You need to return information for all Logs where the LogId has a corresponding record in the Tags table.
The correct way to do this would be to JOIN the Log and Tag tables together, like so:
SELECT *
FROM dbo.LOG_TAG a
INNER JOIN dbo.LOG b ON a.LOG_ID = b.LOG_ID
WHERE a.TAG = #TAG
If you are concerned about returning the same logId multiple times, you can use the DISTINCT keyword in the SELECT statement to filter out the duplicated logIds.
You may also be able to rewrite your dbo.getLogByLogId procedure as a user-defined function (UDF). UDFs can accept a table as a parameter and return a table result.
An introduction to user-defined functions can be found in this article.