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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>Number | Date | Date | Bool
-------+------+------+------
1 | 2302 | 2402 | false
2 | 2303 | 2403 | true
</code>
<code>Number | Date | Date | Bool -------+------+------+------ 1 | 2302 | 2402 | false 2 | 2303 | 2403 | true </code>
Number | Date | Date | Bool
-------+------+------+------
   1   | 2302 | 2402 | false
   2   | 2303 | 2403 | true

Where the string supplied for this would look like

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>(1,2302,2403,0);(2,2303,2403,1)
</code>
<code>(1,2302,2403,0);(2,2303,2403,1) </code>
(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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>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
</code>
<code>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 </code>
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.

1

The message “DBCC execution completed” most likely comes from the DBCC command:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>DBCC CHECKIDENT(ResultTable,RESEED,0)
</code>
<code>DBCC CHECKIDENT(ResultTable,RESEED,0) </code>
DBCC CHECKIDENT(ResultTable,RESEED,0)

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>DBCC CHECKIDENT(ResultTable,RESEED,0) WITH NO_INFOMSGS
</code>
<code>DBCC CHECKIDENT(ResultTable,RESEED,0) WITH NO_INFOMSGS </code>
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.

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật