Search code examples
sqlstored-proceduresplcdbcctds

SQL Server TDS protocol response problem when calling a stored procedure


I am developing an application to communicate between an industrial controller and a Microsoft SQL Server database. I have been given a stored procedure as a starting point that exists in the database.

The database is supplied on a test PC with logins etc already set up. The job of the stored procedure is to take a string from the PLC in a format such as (Number1,Date1,Date2,Bool1;(Number2,Date3,Date4,Bool2), and translate that into a table that accepts data in that format.

For example:

Number | Date | Date | Bool
-------+------+------+------
   1   | 2302 | 2402 | false
   2   | 2303 | 2403 | true

Where the string supplied for this would look like

(1,2302,2403,0);(2,2303,2403,1) 

which is supplied as one string parameter to the stored procedure.

The code to talk to the database from the controller uses the TDS protocol and has already been provided by the PLC vendor with examples of how to use it.

The application code just now is sending fixed strings to the database stored procedure to test the response and look at the format of the data returned to see if we can decode the data to determine the response of the database so we can gauge if it is a success or not when inserting the data.

Everything looks to be working on the database side. When I call the stored procedure with the fixed string, I get a response and when I check the database, the expected values are inserted into the table and everything looks ok.

Now we tried to increase the number of rows of data that we were sending and quickly found that the response from the database was increasing exponentially which eventually ended up in a buffer overflow on the controller end. I piped the response into a string so I could look at it and the results were something like the following

:Checking identity information: current identity value '9'.DESKTOP-9O8SHG7DeserializeIOLDataj«à \DBCC execution completed. If DBCC printed error messages, contact your system administrator.DESKTOP-9O8SHG7DeserializeIOLDataj«Ä5:Checking identity information: current identity value '9'.DESKTOP-9O8SHG7DeserializeIOLDataj«à \DBCC execution completed. If DBCC printed error messages, contact your system administrator.DESKTOP-9O8SHG7DeserializeIOLDataj«Ä5:Checking identity information: current identity value '9'.DESKTOP-9O8SHG7DeserializeIOLDataj«à \DBCC execution completed. If DBCC printed error messages, contact your system administrator.

To me this looks like the output from one of the instructions inside the stored procedure, but I don't see where it comes from or how to turn it off. At the end of the stored procedure, there is a select statement that is supposed to return a number indicating the success of the script and that is appended to the end of the output like this

DeserializeIOLDataj!&ResultÑÿÁyþà

Which will contain the table header and the value from the select statement which is all I was expecting back.

An abridged example of the stored procedure is provided for reference so you can see roughly what it does.

ALTER PROCEDURE [dbo].[DeserializeIOLData] 
    @WorkOrder nvarchar(50), 
    @SerialData nvarchar(max)
AS
BEGIN

BEGIN TRY
    SET NOCOUNT ON;
    
    DECLARE @WorkOrderID VARCHAR(50)
    SET @WorkOrderID = @WorkOrder

    DECLARE @serialIOLData NVARCHAR(MAX)
    SET @serialIOLData = @SerialData

    -- Declare temporary tables for processing of the data internally
    DECLARE @TempTable TABLE(TempData NVARCHAR(MAX));
    DECLARE @ProcessTable TABLE(TempData NVARCHAR(MAX));

    -- Convert the data string into a table array for processing purposes
    INSERT INTO @TempTable 
        SELECT * 
        FROM STRING_SPLIT(@serialIOLData,';')

    -- Remove extra formatting characters from the SQL data received
    UPDATE @TempTable 
    SET TempData = REPLACE(TempData,'(','') -- Remove ( Chars

    UPDATE @TempTable 
    SET TempData = REPLACE(TempData,')','') -- Remove ) Chars


    -- Create temporary variables here to store the various fields of the results in so we can extract them out 1 at a time
    DECLARE @MyCursor CURSOR; -- Pointer variables for working with a table through loop processing
    DECLARE @MyField NVARCHAR(MAX);

    DECLARE @Temp_WorkOrderID VARCHAR(50)
    DECLARE @Temp_IOLSerialID VARCHAR(50)
    DECLARE @Temp_Welder_ID VARCHAR(50)
    DECLARE @Temp_WeldRecipe INT
    DECLARE @Temp_WeldResult INT
    DECLARE @Temp_IOL_StartDate DATETIME2(7)
    DECLARE @Temp_IOL_WeldDate DATETIME2(7)
    DECLARE @Temp_IOL_FinishDate DATETIME2(7)
    DECLARE @Temp_IsComplete INT
    DECLARE @Temp_IOLStatus INT
    DECLARE @Temp_Reject_Code INT
    DECLARE @Temp_WorkValue VARCHAR(max)
    DECLARE @SerialCount INT
    DECLARE @Temp_ChangedOn DATETIME2(7)
    SET @SerialCount = 1

    -- This code loops through all the rows in the temporary table and processes each of them to extract the CSV data from the string
    -- into the final format for insertion into the end table
    BEGIN
        SET @MyCursor = CURSOR FOR
            SELECT TempData FROM @TempTable      

        OPEN @MyCursor 

        FETCH NEXT FROM @MyCursor INTO @MyField

        WHILE @@FETCH_STATUS = 0
        BEGIN
            -- Delete all results from the WorkTable and reseed the index
            -- We do this because we are using the auto increment property of the ID column to identify the position of each of the variables
            DELETE FROM ResultTable

            DBCC CHECKIDENT(ResultTable, RESEED, 0)

            DELETE FROM ResultTable

            -- Split the single result field into the component variables
            INSERT INTO ResultTable 
                SELECT * FROM STRING_SPLIT(@MyField,',')
            --SELECT * FROM ResultTable 

            -- Set a fixed value here since all the records have the same ID
            SET @Temp_WorkOrderID = @WorkOrderID

            -- Get the individual values from the Result table and cast/map them to the correct variables
            -- SET @Temp_WorkValue = (SELECT TOP 1 * FROM @ResultTable)
            -- SET @Temp_IOLSerialID = @Temp_WorkValue

            -- Replace the serial count from the read data string with a counter that we increment in this procedure to enforce the
            -- linearity of the count and allow us to remove the serial number from the sent data and save on string length

            SET @Temp_IOLSerialID = @SerialCount
            SET @SerialCount = @SerialCount + 1 -- Increment by 1 for the next record

            SET @Temp_WorkValue  = (SELECT TempData FROM ResultTable WHERE ID = 1)
            SET @Temp_Welder_ID = @Temp_WorkValue

            SET @Temp_WorkValue  = (SELECT TempData FROM ResultTable WHERE ID = 2)
            SET @Temp_WeldRecipe = CONVERT(INT, @Temp_WorkValue)

            SET @Temp_WorkValue  = (SELECT TempData FROM ResultTable WHERE ID = 3)
            SET @Temp_WeldResult = CONVERT(INT, @Temp_WorkValue)

            SET @Temp_WorkValue  = (SELECT TempData FROM ResultTable WHERE ID = 4)
            SET @Temp_IOL_StartDate = CONVERT(DATETIME2(7), @Temp_WorkValue)

            SET @Temp_WorkValue  = (SELECT TempData FROM ResultTable WHERE ID = 5)
            SET @Temp_IOL_WeldDate = CONVERT(DATETIME2(7), @Temp_WorkValue)

            SET @Temp_WorkValue  = (SELECT TempData FROM ResultTable WHERE ID = 6)
            SET @Temp_IOL_FinishDate = CONVERT(DATETIME2(7), @Temp_WorkValue)

            SET @Temp_WorkValue  = (SELECT TempData FROM ResultTable WHERE ID = 7)
            SET @Temp_IsComplete = (SELECT CONVERT(INT, @Temp_WorkValue))

            SET @Temp_WorkValue  = (SELECT TempData FROM ResultTable WHERE ID = 8)
            SET @Temp_IOLStatus = (SELECT CONVERT(INT, @Temp_WorkValue))

            SET @Temp_WorkValue  = (SELECT TempData FROM ResultTable WHERE ID = 9)
            SET @Temp_Reject_Code = (SELECT CONVERT(INT, @Temp_WorkValue))

            SET @Temp_ChangedOn = (SELECT GETDATE())

            INSERT INTO Tbl_IOL (WorkOrderID, IOLSerialID, Welder_ID, WeldRecipe, WeldResult, IOL_StartDate, IOL_WeldDate, IOL_FinishDate, IsComplete, IOLStatus, Reject_Code, ChangedOn)
            VALUES (@Temp_WorkOrderID, @Temp_IOLSerialID, @Temp_Welder_ID, @Temp_WeldRecipe, @Temp_WeldResult, @Temp_IOL_StartDate, @Temp_IOL_WeldDate, @Temp_IOL_FinishDate, @Temp_IsComplete, @Temp_IOLStatus, @Temp_Reject_Code, @Temp_ChangedOn)

            DELETE FROM ResultTable

            FETCH NEXT FROM @MyCursor INTO @MyField 
        END; 

        CLOSE @MyCursor ;
        DEALLOCATE @MyCursor;
    END;

    DECLARE @Result INT
    SET @Result = 1;

    SELECT @Result AS 'Result';

END TRY

There is a temporary table in the DB that this stored procedure uses to process the strings which is being filled and deleted and reindexed every time for each result set that needs to be inserted. For me the erroneous output must be in the cursor loop towards the bottom end of the table since this is bit of code that runs more when we increase the number of rows.

Hopefully there is an ECHO OFF statement or something similar we can use to get rid of the excess output or a better way to rework the above stored procedure.

Fingers crossed you guys have some good ideas as I am totally stuck.

Appreciate the responses and effort as always.


Solution

  • The message "DBCC execution completed" most likely comes from the DBCC command:

    DBCC CHECKIDENT(ResultTable,RESEED,0)
    

    You can try to add a WITH NO_INFOMSGS argument to suppress the informational messages. Like this:

    DBCC CHECKIDENT(ResultTable,RESEED,0) WITH NO_INFOMSGS
    

    I'd recommend to run the stored procedure in SSMS to see what kind of output / results it generates before and after the changes.


    Overall, the DBCC CHECKIDENT(RESEED) is rather ugly. Maybe you can get rid of it. For example, TRUNCATE TABLE works much much faster than DELETE and it resets the IDENTITY column as well. Just as you want.