2

This is my stored procedure which is called every two mins by a SQL Job.

    USE [MyDB]
GO
/****** Object:  StoredProcedure [dbo].[usp_Process_Invoice_USPs]    Script Date: 27/03/2019 11:39:01 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- ==================================================
-- Author:      Mych
-- Create date: 20/03/2019
-- Description: This stored procedure will create
-- an in-memory table and get any uncompleted rows
-- from the Queue table. For each row found it
-- will construct an Exec statement and execute it.
-- it then marks the row as completed.
-- =================================================
ALTER PROCEDURE [dbo].[usp_Process_Invoice_USPs] 

AS

DECLARE @QueueID INT,
    @ProcName NVARCHAR(100),
    @Username NVARCHAR(50),
    @UserEmail NVARCHAR(100),
    @ParamName NVARCHAR(50),
    @ParamValue INT,
    @QueuedOn Datetime,
    @ParamOut NVarchar(200),
    @RowCnt INT,
    @MaxRows INT,
    @ExecSql NVARCHAR(Max),
    @ResultfromUSP NVARCHAR(250),
    @Return NVARCHAR(250)

SELECT @RowCnt = 1

-- Create an in-memory table - Doing this as this is the only way I can think of to guarantee that rownum will be consecutive
-- the ID column in Tbl_Invoicing_Queue is an Identity column but numbers can be missed out when an insert fails for some reason.

DECLARE @Import TABLE (rownum INT IDENTITY (1, 1) PRIMARY KEY NOT NULL , QueueID INT NOT NULL, ProcName NVARCHAR(100), 
Username NVARCHAR(50), UserEmail NVARCHAR(100), ParamName NVARCHAR(50), ParamValue int, QueuedOn datetime)
-- Insert any uncompleted rows (Progress = 'Queued') from the Queue to the in-memory table
INSERT INTO @Import (QueueID, ProcName, Username, UserEmail, ParamName, ParamValue, QueuedOn) 
SELECT ID, ProcName, Username, UserEmail, ParamName, ParamValue, QueuedOn FROM TBL_Invoicing_Queue WHERE Progress = 'Queued'

-- Find out how many rows were imported.
SELECT @MaxRows=count(*) FROM @Import

--loop through each row... get the field values and construct a sql Exec statement based on the values
--Execute the Exec Statement 
--Write all details and result to the completed table... Progress will be either completed or failed dependant of @ResultfromUSP
--Delete the row from the queued table.

while @RowCnt <= @MaxRows
begin
    SELECT @QueueID = QueueID FROM @Import WHERE rownum = @RowCnt;
    SELECT @ProcName = ProcName FROM @Import WHERE rownum = @RowCnt;
    SELECT @UserName = UserName FROM @Import WHERE rownum = @RowCnt;
    SELECT @UserEmail = UserEmail FROM @Import WHERE rownum = @RowCnt;
    SELECT @ParamName = ParamName FROM @Import WHERE rownum = @RowCnt;
    SELECT @ParamValue = ParamValue FROM @Import WHERE rownum = @RowCnt;
    SELECT @QueuedOn = QueuedOn FROM @Import WHERE rownum = @RowCnt

    Update TBL_Invoicing_Queue SET Progress = 'In Progress - ' + CONVERT(VARCHAR, FORMAT(GETDATE(), 'dd/MM/yyyy HH:mm:ss', 'en-GB')) Where ID = @QueueID

    SET @ExecSql =  N'EXEC dbo.' + @ProcName

    If @ParamName <> ''
    BEGIN
        SET @ParamOut = N'@User = ''' + @UserName + ''', @EmailAddress = ''' + @UserEmail + ''', @' + @ParamName + ' = ' + CAST(@ParamValue AS NVARCHAR(20))
    END
    ELSE
    BEGIN
        SET @ParamOut = N'@User = ''' + @UserName + ''', @EmailAddress = ''' + @UserEmail + ''', @Return = @Return out'
    END

    --Execute our exec statement and get returned result (string) which will either be SUCCESS or a message detailing a failure.
    EXEC sp_executesql @ExecSql, @ParamOut, @ResultfromUSP = @Return out

    --dependant on Result we insert the following into the completed table
    SELECT @ResultfromUSP
    PRINT @ResultfromUSP
    PRINT 'Got result back from USP'

    IF @ResultfromUSP = 'SUCCESS' 
    BEGIN
        Insert INTO TBL_Invoicing_Completed (QID, ProcName, Username, UserEmail, ParamName, ParamValue, QueuedOn, Progress, Result) Values 
        (@QueueID, @ProcName, @UserName, @UserEmail, @ParamName, @ParamValue, @QueuedOn, 'Completed - ' + CONVERT(VARCHAR, FORMAT(GETDATE(), 'dd/MM/yyyy HH:mm:ss', 'en-GB')), @ResultfromUSP)
    END
    ELSE
    BEGIN
        Insert INTO TBL_Invoicing_Completed (QID, ProcName, Username, UserEmail, ParamName, ParamValue, QueuedOn, Progress, Result) Values 
        (@QueueID, @ProcName, @UserName, @UserEmail, @ParamName, @ParamValue, @QueuedOn, 'FAILED - ' + CONVERT(VARCHAR, FORMAT(GETDATE(), 'dd/MM/yyyy HH:mm:ss', 'en-GB')), @ResultfromUSP)
    END
    -- remove the row from the queued table
    DELETE FROM TBL_Invoicing_Queue Where ID = @QueueID
    -- increment the count and loop to next row
    Select @RowCnt = @RowCnt + 1

end

-- NO need to drop @Import as this is cleared automatically

All the various Invoicing USPs that this USP will execute have an output param @Return. The Invoicing USP's return either SUCCESS or ERROR... Details of error This has been tested and works as expected.

The problem is the above USP which calls an Invoicing USP dynamically is not getting the @Return output. I suspect my problem is with:

EXEC sp_executesql @ExecSql, @ParamOut, @ResultfromUSP = @Return out

I have even tried...

EXEC sp_executesql @ExecSql, @ParamOut, @ResultfromUSP out

but I can't seem to figure out why.

Any help appreciated.

UPDATE Thanks for all your comments... This usp is run by a SqlJob every 2 mins. So generally there is only ever one job that it needs to process occasionally there may be two. The Queue table has a primary key on ProcName so users cannot trigger the same Procedure until the one in the queue has finished. If they try they get a message back informing them that a job using this Procedure has been Queued or is In Progress and also gives the User name of who requested that job.

I had to implement this work queue flow as some of the Procs that are requested can themselves take 20 to 30mins to complete. Executing these directly from the client side meant the page could time out waiting for a response.

The procedure above works absolutely fine apart from I get no return from the EXEC sp_executesql …. and so the next bit of code always inserts the queued record into the completed table with a status of FAILED even though it actually was a SUCCESS.

I have got around this by having a switch statement that based on @ParamName does a hard coded EXEC usp_xxxxx instead of building the SQL dynamically and using EXEC sp_executesql….

Mych
  • 2,527
  • 4
  • 36
  • 65
  • I would strongly suggest you reconsider this approach. Using a cursor and dynamic sql like this is not a great way of handling relational data. This should be done set based instead. – Sean Lange Mar 27 '19 at 13:58
  • Using a Cursor is a good suggestion but does not solve my issue. My problem of getting the return from the executed USP would still exist. – Mych Mar 27 '19 at 15:55
  • I think you misread my comment. I urge you NOT to use a cursor or any other looping construct. Doing this process RBAR (row by agonizing row) is just painfully slow. – Sean Lange Mar 27 '19 at 16:13
  • This shouldn't run at all because the call to `sp_executesql` is invalid. The second parameter should be a string containing the parameter list in the form `@param [type], @param [type], ...`, not a list of parameters with values. I consistently get "Incorrect syntax near '='" with this approach. For the love of your own sanity as well as your maintainers, rethink this approach -- dynamic, row-by-row SQL calling stored procedures dynamically either *is* the lowest circle of Hell, or else something very close to it. Consider table-valued parameters and shifting logic to client code. – Jeroen Mostert Mar 28 '19 at 10:48
  • I totally agree with both Sean and Jeroen. You can solve the current issue by correctly execute the `sp_executeSql` ([example here](https://stackoverflow.com/a/41743328/3094533)) but it would be better if you rewrite the entire thing and create your procedures with a set based approach. – Zohar Peled Mar 28 '19 at 10:52

0 Answers0