SQL print line number in comment of dynamically created stored procedure? - sql-server

I have written a script that is several thousands of lines long that I am using to generate some stored procedures dynamically.
I want to reference the script that generated the stored procedures in the comments in the stored procedures, and would like to be able to refer to the line in the script file by inserting the line number of the script file into the comments in the stored procedure file.
So for example if ##line_number gave the line number I want in the code bellow then ##line_number should be 5
1| declare #job varchar(max)
2| SET #job = '/* this is generated dynamicly by _______ */'
3| SET #job = #job + 'SELECT *' + CHAR(10)
4| SET #job = #job + 'FROM ' + #Table_Name + CHAR(10)
5| SET #job = #job + '/* ' + ##line_number + ' */'

You can use TRY / CATCH with a forced error as the CATCH block can return the line number that the error occurred on via the ERROR_LINE() function. The full construct, formatted for readability, is:
BEGIN TRY
;THROW 50000, 'Line#', 1 -- all 3 values are arbitrary, but required
END TRY
BEGIN CATCH
SET #LineNumber = ERROR_LINE()
END CATCH
Now, to get the #LineNumber variable to populate with the line number that it is being set on, you can reduce that construct to a single line as follows:
BEGIN TRY;THROW 50000,'',1;END TRY BEGIN CATCH;SET #Line=ERROR_LINE();END CATCH
Here is a full example of it working:
SET ANSI_NULLS ON
SET NOCOUNT ON
GO
-- Line #1 (of current batch, not of the entire script if GOs are used)
DECLARE #CRLF NCHAR(2) = NCHAR(13) + NCHAR(10),
#SQL1 NVARCHAR(MAX) = '',
#SQL2 NVARCHAR(MAX) = '', -- Line #5
#Line INT = -1 -- default to an invalid line #
SET #SQL1 += N'/********************' + #CRLF
SET #SQL1 += N' *' + #CRLF
SET #SQL1 += N' * Test Auto-' + #CRLF -- Line #10
SET #SQL1 += N' * Generated Proc 1' + #CRLF
BEGIN TRY;THROW 50000,'',1;END TRY BEGIN CATCH;SET #Line=ERROR_LINE();END CATCH
SET #SQL1 += N' * Line #:' + CONVERT(NVARCHAR(10), #Line) + #CRLF
SET #SQL1 += N' *' + #CRLF
SET #SQL1 += N' ********************/' + #CRLF -- Line #15
-- more code here
SET #SQL2 += N'/********************' + #CRLF
SET #SQL2 += N' *' + #CRLF -- Line #20
SET #SQL2 += N' * Test Auto-' + #CRLF
SET #SQL2 += N' * Generated Proc 2' + #CRLF
BEGIN TRY;THROW 50000,'',1;END TRY BEGIN CATCH;SET #Line=ERROR_LINE();END CATCH
SET #SQL2 += N' * Line #:' + CONVERT(NVARCHAR(10), #Line) + #CRLF
SET #SQL2 += N' *' + #CRLF -- Line #25
SET #SQL2 += N' ********************/' + #CRLF
PRINT #SQL1
PRINT #SQL2
GO
The line numbers returned for Proc 1 and Proc 2 are 12 and 23 respectively, which is correct for both.
Please note that the THROW command started in SQL Server 2012. If you are using SQL Server 2005, 2008, or 2008 R2, then you need to use RAISERROR() function instead of THROW.

I changed SolomonRutzky's answer a bit to get is to work in SQL Server versions prior to 2012:
DECLARE #Line INT
SET #Line = 0 BEGIN TRY RAISERROR ('Line#', 11, 1)WITH NOWAIT END TRY BEGIN CATCH SET #Line=ERROR_LINE() END CATCH
PRINT('/* testing ... I messed up somewhere near line: ' + CONVERT(varchar(10), ISNULL(#Line, 0)) + ' */')

Haven't found a built in function to return the current line number so I have started making a function to find the line number for me.
If I get the text of the current running query at the top and declare some variable, and then copy and past the function call and the #LineCounter increment code, I can get the current line number.
DECLARE #var1 NVARCHAR(MAX)
SELECT #var1 = sqltext.TEXT
FROM sys.dm_exec_requests req
CROSS APPLY sys.dm_exec_sql_text(sql_handle) AS sqltext
WHERE req.session_id = ##SPID
DECLARE #LineCounter int
SET #LineCounter = 0
DECLARE #Current_Line_Number int
SET #Current_Line_Number = 0
SET #LineCounter = #LineCounter + 1
SELECT #Current_Line_Number = [MSMS].[dbo].[ReturnLineNumber] (#var1, #LineCounter)
PRINT #Current_Line_Number
This is the function
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author: James J
-- Create date: 11/11/2013
-- Description: Function to return the line number for
-- where the query was called from when passed the query
-- an the count of the times it has already been used.
-- =============================================
ALTER FUNCTION ReturnLineNumber
(
#CurrentQuery nvarchar(max),
#Count int
)
RETURNS int
AS
BEGIN
DECLARE #var1 NVARCHAR(MAX)
DECLARE #functionName nvarchar(30)
SET #functionName = 'ReturnLineNumber'
SET #var1 = #CurrentQuery
DECLARE #LineCount int
SET #LineCount = 0
IF (CHARINDEX(CHAR(13), #var1) > 0)
BEGIN
DECLARE #queryString nvarchar(max)
SET #queryString = #var1
DECLARE #LineIndex int
SET #LineIndex = 1
DECLARE #LineLength int
DECLARE #linestring nvarchar(max)
DECLARE #functioncount int
SET #functioncount = 0
WHILE (#LineIndex > 0)
BEGIN
SET #LineIndex = CHARINDEX(CHAR(13), #queryString)
SET #LineLength = LEN(#queryString) - CHARINDEX(CHAR(13), #queryString)
SET #linestring = SUBSTRING(#queryString, 0, #LineIndex + 1)
SET #queryString = SUBSTRING(#queryString, #LineIndex + 1, #LineLength)
SET #LineCount = #LineCount + 1
IF (CHARINDEX(#functionName, #linestring) > 0)
BEGIN
SET #functioncount = #functioncount + 1
IF (#functioncount = #Count)
BEGIN
RETURN #LineCount
END
END
END
END
RETURN 0
END
GO
This is not a great way to get the line number and I probably should add some more checks to make sure I don't have commented out function calls but this is the closest I have got for the moment.

Related

Error Handling - Determine whether sp_executesql was the source

When an error message is returned by sp_executesql, is there any built-in error handling method/mechanism that can be used to identify that this error was returned by this procedure vs. directly from the containing script?
In the case of the below, the error details identify the error as occurring on line 1 but they don't indicate that the line 1 being referred to is not line 1 from the script itself but rather line 1 in the query passed by the script to sp_executesql.
I'm looking for some way to identify the source so that I can log it accordingly. I'd like to log something like Script x - Call to inner query errored on that query's line 1 instead of the generic (and misleading) Script x errored on line 1.
Demo
-- do other things first
BEGIN TRY
EXECUTE sp_executesql #stmt = N'SELECT 1/0';
END TRY
BEGIN CATCH
SELECT ERROR_NUMBER() AS ErrorNumber
,ERROR_SEVERITY() AS ErrorSeverity
,ERROR_STATE() AS ErrorState
,ERROR_PROCEDURE() AS ErrorProcedure
,ERROR_LINE() AS ErrorLine
,ERROR_MESSAGE() AS ErrorMessage
END CATCH;
Returns:
ErrorNumber ErrorSeverity ErrorState ErrorProcedure
----------- ------------- ----------- ---------------
8134 16 1 NULL
Unfortunately, the call stack isn't available with T-SQL error handling. Consider upvoting this feature request to facilitate capturing T-SQL stack details.
The example below uses a nested TRY/CATCH to raise a user-defined error (message number 50000) when the inner script errors, capturing the available details along with context description ("inner script"). When errors occur in the outer script, the original error is simply re-thrown. Absence of a context and a system error number indicates the outermost script erred, although you could build and raise a user-defined error there instead, including the outer script context description.
BEGIN TRY
BEGIN TRY
EXECUTE sp_executesql #stmt = N'SELECT 1/0;';
END TRY
BEGIN CATCH
DECLARE
#ErrorNumber int
,#ErrorMessage nvarchar(2048)
,#ErrorSeverity int
,#ErrorState int
,#ErrorLine int;
SELECT
#ErrorNumber =ERROR_NUMBER()
,#ErrorMessage =ERROR_MESSAGE()
,#ErrorSeverity = ERROR_SEVERITY()
,#ErrorState =ERROR_STATE()
,#ErrorLine =ERROR_LINE();
RAISERROR('Error %d caught in inner script at line %d: %s'
,#ErrorSeverity
,#ErrorState
,#ErrorNumber
,#ErrorLine
,#ErrorMessage);
END CATCH;
END TRY
BEGIN CATCH
THROW;
END CATCH;
GO
you can use sp_executeSQL's OUTPUT parameter:
DECLARE #ErrorLine NVARCHAR(32)
DECLARE #Params NVARCHAR(150) = '#Return INT OUTPUT'
DECLARE #SQL NVARCHAR(MAX) = ''
SET #SQL = #SQL + ' '
SET #SQL = #SQL + ' BEGIN TRY '
SET #SQL = #SQL + ' SELECT 100 AS First '
SET #SQL = #SQL + ' SELECT 1/0 AS Second '
SET #SQL = #SQL + ' END TRY '
SET #SQL = #SQL + ' BEGIN CATCH '
SET #SQL = #SQL + ' SELECT #Return = ERROR_LINE() '
SET #SQL = #SQL + ' END CATCH '
SET #SQL = #SQL + ' '
EXEC sp_executeSQL #SQL, #Params, #Return = #ErrorLine OUTPUT
SELECT #ErrorLine
this code will show #ErrorLine = 1 regardless of where the error is because technically, the entire SQL is on a single line and it makes the whole thing more complex, but you get the idea...
EDIT: if #ErrorLine is NULL, there's no error in sp_executeSQL.
Here is the solution that returns the line number. We are in a SPROC that takes parameters. Essentially, as a tSQL developer, you will need to guess where issues will occur, normally around arguments to formal parameter inputs.
-- Preamble
CREATE PROCEDURE [Meta].[ValidateTable]
#DatabaseNameInput VARCHAR(100), -- = 'DatabaseNameInput',
#TableNameInput VARCHAR(100), -- = 'TableNameInput',
#SchemaNameInput VARCHAR(100), -- = 'SchemaNameInput',
AS
BEGIN
DECLARE #crlf CHAR(2) = CHAR(13) + CHAR(10),
-----------Database Validity------------------
#IsDatabaseValid BIT,
#DatabaseNumber INTEGER,
#DatabaseNamePredicate NVARCHAR(100),
#CurrentExecutingContext NVARCHAR(40),
#DatabaseValidityExecutable NVARCHAR(100),
#DatabaseParameterString NVARCHAR(50),
-----------Table Validity------------------
#TableObjectIdentity INTEGER,
#TableString NVARCHAR(500),
#TableParameterString NVARCHAR(50),
#TableValidityExecutable NVARCHAR(200),
-----------Error Handling------------------
#ErrorState INTEGER = 0,
#ErrorNumber INTEGER = 0,
#ErrorSeverity INTEGER = 0,
#MyErrorMessage NVARCHAR(150),
#SetMessageText NVARCHAR(1024) = 'No Error Message Text for sys.messages.',
#ErrorDescription NVARCHAR(1024) = 'No error description was given.';
-- Be aware of SQL Injection Risk with no semi-colons at the line tails
SET #TableString = 'N' + '''' + #DatabaseNameInput + '.' + #SchemaNameInput + '.' + #TableNameInput + '''';
SET #DatabaseParameterString = N'#DatabaseNumber INTEGER OUTPUT ';
SET #TableParameterString = N'#TableObjectIdentity INTEGER OUTPUT';
-- Phase 0.0, testing for database existence.
PRINT 'Table Validity Executable: ' + #TableValidityExecutable;
EXECUTE sp_executesql #DatabaseValidityExecutable, #DatabaseParameterString, #DatabaseNumber = #DatabaseNumber OUTPUT;
IF #DatabaseNumber IS NULL
BEGIN
SET #MyErrorMessage = 'The #DatabaseNameInput parameter: "%s" specified by the caller does not exist on this SQL Server - ' + ##SERVERNAME;
EXECUTE sys.sp_addmessage #msgnum = 59802, #severity = 16, #msgtext = #MyErrorMessage, #replace = 'replace', #lang = 'us_english';
RAISERROR(59802, 15, 1, #DatabaseNamePredicate);
END;
-- Phase 0.1, testing for table existence.
PRINT 'Table Validity Executable: ' + #TableValidityExecutable;
EXECUTE sp_executesql #TableValidityExecutable, #TableParameterString, #TableObjectIdentity = #TableObjectIdentity OUTPUT;
IF #TableObjectIdentity IS NULL
BEGIN
SET #MyErrorMessage = 'The #TableNameInput parameter: "%s" specified by the caller does not exist in this database - ' + DB_NAME() +';';
EXECUTE sys.sp_addmessage #msgnum = 59803, #severity = 16, #msgtext = #MyErrorMessage, #replace = 'replace', #lang = 'us_english';
RAISERROR(59803, 15, 1, #TableString);
END;

Challenge with dynamic SQL

I have a procedure that generates dynamic SQL that creates an insert into statement while querying an excel spreadsheet.
The resulting print from the messages screen can be pasted into an ssms window and executes. When I try to execute the SQL from within the stored procedure I get a syntax error as follows:
'SELECT * into TestClient FROM OPENROWSET('Microsoft.ACE.OLEDB.12.0', 'Excel 12.0;HDR=YES;Database=G:\CustomerETL\Employee\PendingETL\ETLEmployeexls.xls;', [Sheet1$])'
Msg 102, Level 15, State 1, Line 15
Incorrect syntax near 'SELECT * into TestClient FROM OPENROWSET('.
Below is the entire stored procedure. I know the problem is in the ticks (within the SET blocks that create the dynamic SQL I just can't figure out where the missing ticks are.
Here is the proc:
USE [ETL]
GO
/****** Object: StoredProcedure [dbo].[ImportExcelSheetForCustomerEmployeeUpdate2] Script Date: 12/19/2017 4:03:05 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[ImportExcelSheetForCustomerEmployeeUpdate2](#BatchID int)
as
--EXEC ImportExcelSheetForCustomerEmployeeUpdate 2
/* -- TRUNCATE TABLE FilesToImport
UPDATE FilesToImport
SET StatusID = 1
*/
-- Jeffery Williams
-- 12/18/2017
DECLARE #FileID int
,#ETLFilename varchar(250)
,#ClientName varchar(100)
,#FileType varchar(5)
,#ColumnCount int
,#RowsToETL int
,#StatusID int
,#Processed bit = 0
,#Count int
,#SQL nvarchar(4000)
,#Sheetname varchar(50) = '[Sheet1$]'
,#CMDSQL as varchar(4000)
,#SQLCmd NVARCHAR(MAX)
SELECT *
FROM FilesToImport
BEGIN
SELECT #Count = count(*)
FROM FilesToImport
WHERE BatchID = #BatchID
AND StatusID = 1
END
PRINT 'Count of records to process: ' + cast(#Count as varchar)
WHILE #Count > 0
BEGIN
BEGIN
SELECT TOP 1 #FileID = FileID, #ETLFilename = ETLFilename, #ClientName = ClientName
,#FileType = FileType, #ColumnCount = ColumnCount, #RowsToETL = RowsToETL
FROM FilesToImport
WHERE StatusID = 1
AND BatchID = #BatchID
END
-- Rename the file
set #CMDSQL = 'rename G:\CustomerETL\Employee\PendingETL\' + #ETLFilename + ' ETLEmployeexls.xls'
exec master..xp_cmdshell #CMDSQL
--PRINT cast(#cmdsql as varchar(4000))
-- Ciode below generates our select. Need to add an INTO clause and create a staging table for each import. Prior to this step we need to rename the file.
SET #SQL = ''''
SET #SQL = #SQL + 'SELECT * into ' + coalesce(#ClientName, 'TestClient') + ' FROM OPENROWSET('
SET #SQL = #SQL + ''''
SET #SQL = #SQL + '''' + 'Microsoft.ACE.OLEDB.12.0' + '''' --+ ', '
-- Excel 12.0;HDR=NO;Database=g:\UnZip\ImportSampleXLSX.xlsx;' + ''
SET #SQL = #SQL + '''' + ', '
SET #SQL = #SQL + '''' + '''Excel 12.0;HDR=YES;Database=G:\CustomerETL\Employee\PendingETL\ETLEmployeexls.xls;''' + '''' + ', ' + #Sheetname + ')'
SET #SQL = #SQL + ''''
PRINT cast(#SQL as varchar(8000))
EXEC sp_executesql #SQL
set #CMDSQL = 'rename G:\CustomerETL\Employee\PendingETL\ETLEmployeexls.xls ' + #ETLFilename
exec master..xp_cmdshell #CMDSQL
UPDATE FilesToImport
SET StatusID = 2
WHERE FileID = #FileID
/* -- TRUNCATE TABLE FilesToImport
UPDATE FilesToImport
SET StatusID = 1
*/
SET #Count = (#Count - 1)
CONTINUE
END
I am posting this as an answer but it should be comment. When I tried adding this as a comment StackOveflow kept thinking that I was trying to add #count as an email target.
In your code:
WHILE #Count > 0
BEGIN
BEGIN
SELECT TOP 1 #FileID = FileID, #ETLFilename = ETLFilename, #ClientName = ClientName
,#FileType = FileType, #ColumnCount = ColumnCount, #RowsToETL = RowsToETL
FROM FilesToImport
WHERE StatusID = 1
AND BatchID = #BatchID
END
you are not updating the value of #count. This will either never loop or loop forever. You probably want to add a statement (right before the end) such as this:
Set #count= ##rowcount;
Ben

Dynamic Insert Loop

As I understand it, the following code:
declare #csvnmbr nvarchar(3)
declare #sql nvarchar(400)
set #csvnmbr = 1
set #sql = 'BULK INSERT #tablename FROM ''C:\TEMP\'+#csvnmbr+'.csv''
WITH (FIELDTERMINATOR = '','')'
while #csvnmbr < 4
begin
print #sql
set #csvnmbr = #csvnmbr + 1
end
GO
Should print:
BULK INSERT #tablename FROM 'C:\TEMP\1.csv' WITH (FIELDTERMINATOR = ',')
BULK INSERT #tablename FROM 'C:\TEMP\2.csv' WITH (FIELDTERMINATOR = ',')
BULK INSERT #tablename FROM 'C:\TEMP\3.csv' WITH (FIELDTERMINATOR = ',')
but it doesn't, it prints:
BULK INSERT #tablename FROM 'C:\TEMP\1.csv' WITH (FIELDTERMINATOR = ',')
BULK INSERT #tablename FROM 'C:\TEMP\1.csv' WITH (FIELDTERMINATOR = ',')
BULK INSERT #tablename FROM 'C:\TEMP\1.csv' WITH (FIELDTERMINATOR = ',')
Does anyone know why the loop won't print the number sequence but just sticks to the first csv number instead?
If I do this though:
declare #csvnmbr nvarchar(3)
declare #sql nvarchar(400)
set #csvnmbr = 1
set #sql = 'HI'
while #csvnmbr < 4
begin
print #sql
print #csvnmbr
set #csvnmbr = #csvnmbr + 1
end
GO
There is no apparent need to write the #sql inside the loop. Any idea why the fact that the #sql statement is dynamic means it needs to be reset each time?
Because it is set once before loop starts. You need to set #sql inside loop:
DECLARE #csvnmbr INT = 1
,#sql NVARCHAR(MAX);
WHILE #csvnmbr < 4
BEGIN
SET #sql = N'BULK INSERT #tablename FROM ''C:\TEMP\'' +
CAST(#csvnmbr AS NVARCHAR(10)) +
'.csv'' WITH (FIELDTERMINATOR = '','')';
PRINT #sql;
SET #csvnmbr += 1;
END
About your question why it needs to be inside loop:
declare #csvnmbr nvarchar(3)
declare #sql nvarchar(400)
set #csvnmbr = 1
/* At this point #sql if fixed string, during concatenation #csvnmbr was
replaced by its actual value */
set #sql = 'BULK INSERT #tablename FROM ''C:\TEMP\'+#csvnmbr+'.csv''
WITH (FIELDTERMINATOR = '','')'
while #csvnmbr < 4
begin
print #sql /* Here sql will be always the same you need to set it again */
set #csvnmbr = #csvnmbr + 1
end
What you trying to achieve is passing string by reference. You can mimic it like this:
DECLARE #csvnmbr NVARCHAR(3) = '1'
,#sql NVARCHAR(MAX);
SET #sql =
N'BULK INSERT #tablename FROM ''C:\TEMP\#csvnmbr.csv'' WITH (FIELDTERMINATOR = '','')';
WHILE #csvnmbr < 4
BEGIN
/* Warning!!! This will execute code in #sql*/
EXEC [dbo].[sp_executesql]
#sql,
N'#csvnmbr nvarchar(3)',
#cvnmbr; /* Will substitute #csvnmbr with passed value */
SET #csvnmbr += 1;
END
Keep your statement inside the while loop
declare #csvnmbr nvarchar(3)
declare #sql nvarchar(400)
set #csvnmbr = 1
while #csvnmbr < 4
begin
set #sql = 'BULK INSERT #tablename FROM ''C:\TEMP\'+#csvnmbr+'.csv''
WITH (FIELDTERMINATOR = '','')'
print #sql
set #csvnmbr = #csvnmbr + 1
end
GO
the while loop iteration happens between the while loop condition begin and end blocks.
WHILE (Some Condition) --<-- first the condition is checked
BEGIN
--<-- if the while condition is evaluated to true
-- code between the BEGIN and END is executed
-- it keeps getting executing until while condition returns false
END
You need to fill #sql in each iteration:
DECLARE #csvnmbr NVARCHAR(3);
DECLARE #sql NVARCHAR(400);
SET #csvnmbr = 1;
SET #sql = 'BULK INSERT #tablename FROM ''C:\TEMP\' + #csvnmbr + '.csv''
WITH (FIELDTERMINATOR = '','')';
WHILE #csvnmbr < 4
BEGIN
PRINT #sql;
SET #csvnmbr = #csvnmbr + 1;
SET #sql = 'BULK INSERT #tablename FROM ''C:\TEMP\' + #csvnmbr + '.csv''
WITH (FIELDTERMINATOR = '','')';
END;
GO

Msg 102, Level 15, State 1, Line 34 Incorrect syntax near '.'

I am getting above error while executing query as following, which is a stored procedure command line execution
exec sp_Bind_Division_Store_Camera_On_Filter_In_Liveview
null, null, null, null
Stored procedure is as following
ALTER PROCEDURE [dbo].[sp_Bind_Division_Store_Camera_On_Filter_In_Liveview]
#division varchar(45),
#store varchar(45),
#camera varchar(68),
#group varchar(100)
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
DECLARE
#BaseQuery nvarchar(max) = 'select
distinct tblcameradetails.name as CameraName,
tblcameradetails.IsDeviceEnabled,
tblcameradetails.cameraID,
tblcameradetails.deviceIP,
tblmediasourcedetails.StreamName,
tblmediasourcedetails.streamID,
tblcameradetails.Model_ID,
tblcameradetails.IsHidden,
tblcameradetails.hasPTZcapability,
tblcameradetails.CameraModelNo,
tblcameradetails.username as CameraUserName,
tblcameradetails.hasPTZCycle,
tblcameradetails.hasPreset,
tblcameradetails.password as CameraPassword,
tblmediasourcedetails.isRecordingStarted as IsRecordingOn,
tblcameradetails.IsCovert,
tblcameradetails.constCameraName,
tblmediasourcedetails.constStreamName,
tblstoredetails.Store_ID,
tblsystemlocationdetails.division,
tblstoredetails.Store_Name,
tblstoredetails.Store_IP,
tblstoreconfiguration.Liveview_Session_Timeout
from
tblmediasourcedetails,
tblcameradetails,
tblcameragroupdetails,
tblgroupdetails,
tblvssaddsourcedetails,
tblsystemlocationdetails,
tblstoredetails,
tblstoreconfiguration'
, #ParamList nvarchar(max) = N'#p1 varchar(45), #p2 varchar(45), #p3 varchar(68), #p4 varchar(100)'
, #WhereClause nvarchar(max) = N'where
tblmediasourcedetails.cameraID=tblcameradetails.cameraID
and tblmediasourcedetails.streamID=tblvssaddsourcedetails.streamID
and tblcameradetails.cameraID = tblcameragroupdetails.cameraID
and tblcameragroupdetails.groupID=tblgroupdetails.groupID
and tblstoredetails.Store_ID= tblcameradetails.Store_ID
and tblsystemlocationdetails.Store_ID= tblstoredetails.Store_ID
and tblstoredetails.Store_ID=tblstoreconfiguration.Store_ID and 1=1'
, #OrderByClause nvarchar(100) = 'order by CameraName';
IF #division IS NOT NULL
BEGIN
SET #WhereClause = #WhereClause + ' and division like (#p1)';
END
IF #store IS NOT NULL
BEGIN
SET #WhereClause = #WhereClause + ' and (store like (#p2))';
END
IF #camera IS NOT NULL
BEGIN
SET #WhereClause = #WhereClause + ' and tblcameradetails.name like (#p3)';
END
IF #group IS NOT NULL
BEGIN
SET #WhereClause = #WhereClause + ' and tblgroupdetails.name in (#p4)';
END
SET #BaseQuery = #BaseQuery + #WhereClause + #OrderByClause;
EXECUTE sp_executesql #BaseQuery, #ParamList, #p1 = #division, #p2 = #store, #p3 = #camera, #p4 = #group;
END
Now error shows me that on line 34 error at syntax near '.', but I found no problem at all, because I am run this query form command prompt very well same applied in stored procedure, it will raised error. Please help me to solved out this.
You can easily print your variable #BaseQuery to check the resulting query.
the query in the variable #BaseQuery looks like this
select
distinct tblcameradetails.name as CameraName,
tblcameradetails.IsDeviceEnabled,
tblcameradetails.cameraID,
tblcameradetails.deviceIP,
tblmediasourcedetails.StreamName,
tblmediasourcedetails.streamID,
tblcameradetails.Model_ID,
tblcameradetails.IsHidden,
tblcameradetails.hasPTZcapability,
tblcameradetails.CameraModelNo,
tblcameradetails.username as CameraUserName,
tblcameradetails.hasPTZCycle,
tblcameradetails.hasPreset,
tblcameradetails.password as CameraPassword,
tblmediasourcedetails.isRecordingStarted as IsRecordingOn,
tblcameradetails.IsCovert,
tblcameradetails.constCameraName,
tblmediasourcedetails.constStreamName,
tblstoredetails.Store_ID,
tblsystemlocationdetails.division,
tblstoredetails.Store_Name,
tblstoredetails.Store_IP,
tblstoreconfiguration.Liveview_Session_Timeout
from
tblmediasourcedetails,
tblcameradetails,
tblcameragroupdetails,
tblgroupdetails,
tblvssaddsourcedetails,
tblsystemlocationdetails,
tblstoredetails,
tblstoreconfigurationwhere
tblmediasourcedetails.cameraID=tblcameradetails.cameraID
and tblmediasourcedetails.streamID=tblvssaddsourcedetails.streamID
and tblcameradetails.cameraID = tblcameragroupdetails.cameraID
and tblcameragroupdetails.groupID=tblgroupdetails.groupID
and tblstoredetails.Store_ID= tblcameradetails.Store_ID
and tblsystemlocationdetails.Store_ID= tblstoredetails.Store_ID
and tblstoredetails.Store_ID=tblstoreconfiguration.Store_ID and 1=1order by CameraName
You can see that you are missing a space before the WHERE and ORDER BY clause
You need to add a space in the starting of your WHERE and ORDER BY variables like this
#WhereClause nvarchar(max) = N' where
#OrderByClause nvarchar(100) = ' order by CameraName';
Try to put some PRINT in the middle of the query.
For example PRINT #BaseQuery after SET #BaseQuery = #BaseQuery + #WhereClause + #OrderByClause;
Because maybe you need a space in some variable (for example N' where...1=1' instead of N'where...1=1' )

How do I dynamically build a like clause in an executable sql stored procedure that uses EXEC sp_executesql?

The following stored procedure works correctly execpt when I pass in the #NameSubstring parameter. I know I am not dynamically building the like clause properly. How can I build the like clause when this parameter also needs to be passed as a parameter in the EXEC sp_executesql call near the bottom of the procedure?
ALTER PROCEDURE [dbo].[spGetAutoCompleteList]
(
#AutoCompleteID int,
#StatusFlag int,
#NameSubstring varchar(100),
#CompanyID int,
#ReturnMappings bit,
#ReturnData bit
)
AS
DECLARE #ErrorCode int,
#GetMappings nvarchar(500),
#Debug bit,
#Select AS NVARCHAR(4000),
#From AS NVARCHAR(4000),
#Where AS NVARCHAR(4000),
#Sql AS NVARCHAR(4000),
#Parms AS NVARCHAR(4000)
SET #ErrorCode = 0
SET #Debug = 1
BEGIN TRAN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
IF #AutoCompleteID IS NOT NULL OR #StatusFlag IS NOT NULL OR #NameSubstring IS NOT NULL
BEGIN
SET #Select = '
SELECT ac.AutoCompleteID,
ac.AutoCompleteName,
ac.CompanyID,
ac.StatusFlag,
ac.OwnerOperID,
ac.CreateDT,
ac.CreateOperID,
ac.UpdateDT,
ac.UpdateOperID,
ac.SubmitOperID,
ac.SubmitDT,
ac.ReviewComments'
SET #GetMappings = '
Select ac.AutoCompleteID'
IF #ReturnData = 1
BEGIN
SET #Select = #Select + '
, ac.AutoCompleteData'
END
SET #From = '
FROM tbAutoComplete ac'
SET #Where = '
WHERE 1=1'
IF #AutoCompleteID IS NOT NULL
BEGIN
SET #Where = #Where + '
AND ac.AutoCompleteID = CAST(#AutoCompleteID AS nvarchar)'
END
IF #StatusFlag IS NOT NULL
BEGIN
SET #Where = #Where + '
AND ac.StatusFlag = CAST(#StatusFlag AS nvarchar)'
END
IF #NameSubstring IS NOT NULL
BEGIN
SET #Where = #Where + '
AND ac.AutoCompleteName like #NameSubstring' + '%'
END
SET #Where = #Where + '
AND ac.CompanyID = + CAST(#CompanyID AS nvarchar)'
SET #Sql = #Select + #From + #Where
SET #Parms = '
#AutoCompleteID int,
#StatusFlag int,
#NameSubstring varchar(100),
#CompanyID int'
EXEC sp_executesql #Sql,
#Parms,
#AutoCompleteID,
#StatusFlag,
#NameSubstring,
#CompanyID
IF #ReturnMappings = 1
BEGIN
SET #GetMappings = 'Select * FROM tbAutoCompleteMap acm WHERE acm.AutoCompleteID IN(' + #GetMappings + #From + #Where + ')'
--EXEC sp_executesql #GetMappings
END
IF #Debug = 1
BEGIN
PRINT #GetMappings
PRINT #Sql
END
END
SELECT #ErrorCode = #ErrorCode + ##ERROR
IF #ErrorCode <> 0
BEGIN
SELECT '<FaultClass>1</FaultClass><FaultCode>1</FaultCode>'
+ '<FaultDesc>Internal Database Error.</FaultDesc>'
+ '<FaultDebugInfo>(spGetAutoCompleteList): There was an error while trying to SELECT from tbAutoComplete.</FaultDebugInfo>'
ROLLBACK TRAN
RETURN
END
COMMIT TRAN
#NameString needs to be outside of the quotes. To get #NameString% enclosed in quotes, you use two single quotes to escape the quote character as a literal.
SET #Where = #Where + '
AND ac.AutoCompleteName like ''' + #NameSubstring + '%'''
To avoid SQL injection, do not use concatenation when adding the parameter to your SQL statement. I strongly recommend that you use this format:
IF #NameSubstring IS NOT NULL BEGIN
SET #Where += 'AND ac.AutoCompleteName LIKE #NameSubstring + char(37)'
END
By using char(37) instead of '%' you avoid having to escape the apostrophes around the string literal
If you wanted to put a wildcard at either side, then you would use
IF #NameSubstring IS NOT NULL BEGIN
SET #Where += 'AND ac.AutoCompleteName LIKE char(37) + #NameSubstring + char(37)'
END
-----------------------------------------------------------------------------
In case someone believes I am wrong, here's proof that concatenation is a risk.
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[TestInjection]') AND type in (N'U')) BEGIN
create table TestInjection(ID int, Value nvarchar(10))
insert into TestInjection (ID,Value)
Values
(1,'Tom'),
(2,'Fred'),
(3,'Betty'),
(4,'Betty2'),
(5,'Betty3'),
(6,'George')
END
declare #NameSubstring nvarchar(1000) = 'Bet'
--declare #NameSubstring nvarchar(1000) = 'Bet%'';delete from TestInjection;select * from TestInjection where value = ''x'
declare #ID int = 2
Declare #sql nvarchar(1000) = 'select * from TestInjection where ID > #ID '
SET #sql +=' AND [Value] like ''' + #NameSubstring + '%'''
Declare #params nvarchar(100) = '#ID int'
exec sp_executesql #sql, #params, #ID
select * from TestInjection
Run it the first time and you will get a resultset with 3 records, and another with all 6 records.
Now swap the declaration of #NameSubstring to the alternative, and re-run. All data in the table has been deleted.
If on the other hand you write your code like:
declare #NameSubstring nvarchar(1000) = 'Bet'
--declare #NameSubstring nvarchar(1000) = 'Bet%'';delete from TestInjection;select * from TestInjection where value = ''x'
declare #ID int = 2
Declare #sql nvarchar(1000) = 'select * from TestInjection where ID > #ID '
SET #sql +=' AND [Value] LIKE #NameSubstring + char(37)'
Declare #params nvarchar(100) = '#ID int, #NameSubstring nvarchar(1000)'
exec sp_executesql #sql, #params, #ID, #NameSubstring
select * from TestInjection
Then you still get the 3 records returned the first time, but you don't lose your data when you change the declaration.
SET #Where = #Where + 'AND ac.AutoCompleteName like ''%' + #NameSubstring + '%'''
So, you are asking how to specify parameters when you use dynamic queries and sp_executesql ?
It can be done, like this:
DECLARE /* ... */
SET #SQLString = N'SELECT #LastlnameOUT = max(lname) FROM pubs.dbo.employee WHERE job_lvl = #level'
SET #ParmDefinition = N'#level tinyint, #LastlnameOUT varchar(30) OUTPUT'
SET #IntVariable = 35
EXECUTE sp_executesql #SQLString, #ParmDefinition, #level = #IntVariable, #LastlnameOUT=#Lastlname OUTPUT
You can read more about it here: http://support.microsoft.com/kb/262499
Perhaps this wouldn't be an issue if you weren't using dynamic SQL. It looks to me like a vanilla query would work just as well and be much more straightforward to read and debug. Consider the following:
SELECT ac.AutoCompleteID,
ac.AutoCompleteName,
ac.CompanyID,
ac.StatusFlag,
ac.OwnerOperID,
ac.CreateDT,
ac.CreateOperID,
ac.UpdateDT,
ac.UpdateOperID,
ac.SubmitOperID,
ac.SubmitDT,
ac.ReviewComments
FROM tbAutoComplete ac
WHERE ((ac.AutoCompleteID = CAST(#AutoCompleteID AS nvarchar) OR (#AutoCompleteID IS NULL))
AND ((ac.StatusFlag = CAST(#StatusFlag AS nvarchar)) OR (#StatusFlag IS NULL))
AND ((ac.AutoCompleteName like #NameSubstring + '%') OR (#NameSubstring IS NULL))
AND ((ac.CompanyID = CAST(#CompanyID AS nvarchar)) OR (#CompanyID IS NULL))
This is much simpler, clearer etc. Good luck!

Resources