A single quote in a column breaks the dynamic string - sql-server

Please help me with a simple formatting of a dynamic string that I'm building so I can run multiple statements at once.
Here is the exact code I have which is breaking when the column value has a single quote and I'm unable to wrap the column text with the proper quotation. I tried using double quotes and quotename function but none worked.
declare #str varchar(max)
set #str = ''
declare #addgo varchar(20)
set #addgo = 'GO'
declare #newline varchar(50)
set #newline = char(13)+char(10)
select #str = coalesce(#str, '') + 'sp_addmessage #msgnum = ' + convert(varchar(50), error) + ' , #severity = 16' + ', #msgtext = N''' + "description" + ''''
+ #newline + #addgo + #newline from [msdb].dbo.sysmessages
where error = '777970005'
print #str
Result:
sp_addmessage #msgnum = 777970005 ,#severity = 16,#msgtext = N'Management group with id '%s' is not a member of this Data Warehouse or you do not have permissions to perform actions for that management group'
GO

Like #KamilG said, use the replace function:
select #str= coalesce(#str,'')+'sp_addmessage #msgnum = '+convert(varchar(50),error)+' ,#severity = 16'+',#msgtext = N'''+replace(description,'''','''''')+''''
+ #newline+#addgo+#newline from [msdb].dbo.sysmessages

Related

How to use while loop to iterate through each table name in SQL Server?

I'm working with Dynamic SQL (still in the learning phase) and I'm stuck at a part where I need to use a WHILE loop:
SET #tableName = (SELECT DISTINCT TableName FROM #dataStructure)
Here basically I want to make sure that the operations inside the while loop should occur for all the tables in the #tableName (defined above). I don't know how I can give this condition as an input for the while loop.
WHILE() #HOW CAN I PUT THE CONDITION HERE????
BEGIN
SET #str = ''
SET #sqlstr = ''
SELECT #table = TableName FROM #dataStructure
SET #str = 'UPDATE a0' + char(13) + char(10)
+ ' SET a0.Mjolnir_Source_ID = CONCAT( '
SELECT #str = #str + IIF(ReferenceTable IS NULL, 'a0.' + columnName , alias + '.Mjolnir_Source_ID') + ','
FROM #dataStructure
WHERE TableName = #tableName AND ReferenceTable IS NOT NULL
ORDER BY columnName
SELECT #str = #str + ') FROM ' + #table + ' a0'
SELECT #sqlstr = #sqlstr + +
+ ' INNER JOIN ' + QUOTENAME(#U4SM_db_name) + '.dbo.' + QUOTENAME(ReferenceTable) + ' ' + alias + char(13) + char(10)
+ ' ON a0.' + columnName + ' = ' + alias + '.' + ReferenceColumn + char(13) + char(10)
FROM #dataStructure
WHERE TableName = #tableName AND ReferenceTable IS NOT NULL
ORDER BY columnPosition
select #str + #sqlstr
select #sqlstr
SET #tableName = #tableName + 1
END
Can anyone please help me out here?
Here's an example of a WHILE loop. Basically, you get the first TableName, then if it's NOT NULL, you do your functions. Then get the next table name, and repeat as necessary.
DECLARE #CurrentTableName nvarchar(100)
DECLARE #CustomSQL nvarchar(4000)
SET #CurrentTableName = (SELECT TOP 1 TableName FROM #dataStructure ORDER BY TableName)
WHILE #CurrentTableName IS NOT NULL
BEGIN
SET #CustomSQL = 'SELECT TOP 10 * FROM ' + #CurrentTableName
EXEC (#CustomSQL)
SET #CurrentTableName = (SELECT TOP 1 TableName FROM #dataStructure WHERE TableName > #CurrentTableName ORDER BY TableName)
END
Note that SQL commands often cannot contain variable names in key spots (e.g., SELECT * FROM #tableName). Instead, you save it as an SQL string (what I've called #CustomSQL above) and then EXEC it (put brackets around the variable name though).
Edit: Do this on a test site first before production, and know where the 'cancel query' button is. It's not often, but it's also not unknown, that the 'getting the next row' part isn't properly written and it just runs in a perpetual loop.
FETCH CURSOR with WHILE. Example:
DECLARE myCursor CURSOR FOR
SELECT DISTINCT TableName FROM #dataStructure;
OPEN myCursor;
FETCH NEXT FROM myCursor INTO #table:Name;
WHILE ##FETCH_STATUS = 0
BEGIN
Print ' ' + #TableName
FETCH NEXT FROM myCursor INTO #TableName;
END;
CLOSE myCursor;
DEALLOCATE myCursor;
GO
Don't recreate the wheel unless you need a better wheel:
sp_MSforeachtable
https://www.sqlshack.com/an-introduction-to-sp_msforeachtable-run-commands-iteratively-through-all-tables-in-a-database/
If you are worried about using an undocumented procedure in production that might change in the future, simply script it out and create your own custom named version.

Error while using variable declaration Incorrect syntax near '+#TABLENAME1+'

DECLARE #SQLSTRING VARCHAR(1500);
DECLARE #TABLENAME1 VARCHAR(30)='NOV19_COMBINE'
---------------TABLE CREATION WITH FILE NAME--------------------------
SET #SQLSTRING = 'SELECT
CONVERT(VARCHAR('+ cast((select max(len(EMAIL)) from '+#TABLENAME1+' ) as VARCHAR(50))+'), EMAIL ) AS EMAIL,
IDENTITY (INT,1,1) AS RECORDID
INTO FOI_'+#TABLENAME1+'_CONV
FROM '+#TABLENAME1+' A'
PRINT #SQLSTRING
Error:
Msg 102, Level 15, State 1, Line 8
Incorrect syntax near '+#TABLENAME1+'.
You have an issue here:
CONVERT(VARCHAR('+ cast((select max(len(EMAIL)) from ' + #TABLENAME1 + ' ) as VARCHAR(50))+')
where you are trying to select from a table defined in #TABLENAME1. That also needs to be part of your dynamic SQL.
However you have another issue with your convert(varchar( code in that you cannot use a variable as as the length to varchar(). I suggest using varchar(max) because that only uses the storage required.
I have also made your dynamic SQL safe from injection with the use of QUOTENAME which I recommend you use in future.
Fixed version:
DECLARE #SQLSTRING VARCHAR(1500);
DECLARE #TABLENAME1 VARCHAR(30) = 'NOV19_COMBINE'
---------------TABLE CREATION WITH FILE NAME--------------------------
SET #SQLSTRING = 'SELECT CONVERT(VARCHAR(max), EMAIL) AS EMAIL, IDENTITY (INT,1,1) AS RECORDID INTO '
+ QUOTENAME('FOI_' + #TABLENAME1 + '_CONV') + ' FROM '
+ QUOTENAME(#TABLENAME1) + ' A'
PRINT #SQLSTRING
There is no reason I can think of to do it this way, but as an academic exercise, if one really needed the exact length of the EMAIL column then one would use the following query:
declare #SQLSTRING nvarchar(max), #TABLENAME1 VARCHAR(30) = 'NOV19_COMBINE', #EMAILLENGTH int
SET #SQLSTRING = 'SELECT #Length = max(len(EMAIL)) from ' + QUOTENAME(#TABLENAME1)
EXECUTE sp_executesql #SQLSTRING, N'#Length int OUTPUT', #Length = #EMAILLENGTH OUTPUT
SET #SQLSTRING = 'SELECT CONVERT(VARCHAR(' + convert(varchar(4),#EMAILLENGTH) + '), EMAIL) AS EMAIL'
+ ', IDENTITY (INT,1,1) AS RECORDID'
+ ' INTO ' + QUOTENAME('FOI_' + #TABLENAME1 + '_CONV')
+ ' FROM ' + QUOTENAME(#TABLENAME1) + ' A'
PRINT #SQLSTRING
This requires 2 sections of dynamic SQL, the first to find the length of the EMAIL column, which is then used to built the dynamic SQL for the actual query.

SQL Server : INSTEAD OF UPDATE trigger: dynamic code generation; inserted table not available

I have the following with an INSTEAD OF UPDATE trigger which dynamically generates code and then executes it. The problem is that when the code is executed I always get the error message:
invalid object name 'inserted'
The trigger code is like this:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER Trigger [dbo].[TACMasterLayouts_VersionVisibilityHandling_updates]
ON [dbo].[TACMasterLayouts_VersionVisibilityHandling]
INSTEAD OF UPDATE
AS
IF ##rowcount = 0
RETURN;
SET NOCOUNT ON
DECLARE #tableToUpdate AS NVARCHAR(MAX) = 'dbo.tacmasterlayouts';
DECLARE #UpdateStatement AS NVARCHAR(MAX)
DECLARE #IdentityField AS NVARCHAR(MAX) = (SELECT Name
FROM sys.columns
WHERE [object_id] = OBJECT_ID('dbo.tacmasterlayouts')
AND is_identity = 1)
SET #UpdateStatement = 'update ' + #tableToUpdate + ' set ';
DECLARE #Fields AS NVARCHAR(MAX)
SELECT #Fields = COALESCE(#Fields + ', ', '') + #tableToUpdate + '.' + Name + ' = inserted.' + Name
FROM sys.columns
WHERE [object_id] = OBJECT_ID('dbo.tacmasterlayouts')
AND Name != #IdentityField;
SET #UpdateStatement = #UpdateStatement + #fields
SET #UpdateStatement = #UpdateStatement + ' FROM ' + #tableToUpdate + ', inserted'
SET #UpdateStatement = #UpdateStatement + ' where ' + #tableToUpdate + '.' + #IdentityField + ' = inserted.' + #IdentityField
EXECUTE sp_executesql #UpdateStatement
The dynamic code looks like this:
UPDATE TACMasterLayouts SET
TACMasterLayouts.TACMasterLayoutName = inserted.TACMasterLayoutName,
TACMasterLayouts.TACMasterLayoutDestinationFileName = inserted.TACMasterLayoutDestinationFileName,
TACMasterLayouts.LayoutTypeCategoryId = inserted.LayoutTypeCategoryId,
TACMasterLayouts.LayoutTypeId = inserted.LayoutTypeId,
TACMasterLayouts.TACMasterLayoutDescription = inserted.TACMasterLayoutDescription,
TACMasterLayouts.TACMasterLayoutJasper = inserted.TACMasterLayoutJasper,
TACMasterLayouts.TACMasterLayoutJrxml = inserted.TACMasterLayoutJrxml,
TACMasterLayouts.TACMasterLayoutHtml = inserted.TACMasterLayoutHtml,
TACMasterLayouts.TACMasterLayoutDontClone = inserted.TACMasterLayoutDontClone,
TACMasterLayouts.TACMasterLayoutIsSubReport = inserted.TACMasterLayoutIsSubReport,
TACMasterLayouts.TACMasterLayoutVersion = inserted.TACMasterLayoutVersion,
TACMasterLayouts.TACMasterLayoutKey = inserted.TACMasterLayoutKey,
TACMasterLayouts.TACMasterLayoutTimestampLastModified = inserted.TACMasterLayoutTimestampLastModified,
TACMasterLayouts.TACMasterLayoutHash = inserted.TACMasterLayoutHash,
TACMasterLayouts.TACMasterLayoutTimestampLastCheckout = inserted.TACMasterLayoutTimestampLastCheckout,
TACMasterLayouts.TACMasterLayoutCheckedOutByUserId = inserted.TACMasterLayoutCheckedOutByUserId,
TACMasterLayouts.TACMasterLayoutIsCheckedIn = inserted.TACMasterLayoutIsCheckedIn,
TACMasterLayouts.TACMasterLayoutCheckedInByUserId = inserted.TACMasterLayoutCheckedInByUserId,
TACMasterLayouts.TACMasterLayoutCheckedOutFolderName = inserted.TACMasterLayoutCheckedOutFolderName
FROM TACMasterLayouts, inserted
WHERE TACMAsterlayouts.TACMasterLayoutId = inserted.TACMasterLayoutId;
Hint:
If I am statically entering the dynamically generated code within the trigger, it works fine. so the code itself is ok.
Question: why is inserted not available in this situation? And how to fix it?
When dynamic sql is executed it has it's own scope like calling another stored procedure, so it can not reference what is in the Trigger's scope like inserted, deleted, or local variables etc. You would have to include a literal value in the dynamic sql string for the required key value and also be prepared to handle more than 1 row in inserted which caused the firing of the trigger.
As the above answers didn't gave me the desired solution, I digged further and found the following - cool - solution (thanks to the following article: https://www.sqlteam.com/forums/topic.asp?TOPIC_ID=11318):
If you need to pass the inserted table into dynamic SQL code (to the sp_executesql stored procedure), do a:
select * from inserted into #inserted
and then you can access it immediately within the sp_executesql under #inserted. No need to pass this as a parameter to the sp_executesql.
the final - working - code looks like this:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER Trigger [dbo].[TACMasterLayouts_VersionVisibilityHandling_updates] on [dbo].[TACMasterLayouts_VersionVisibilityHandling] INSTEAD OF update
as
if ##rowcount = 0 return;
SET NOCOUNT ON
Declare #tableToUpdate AS Nvarchar(max) = 'dbo.tacmasterlayouts';
Declare #UpdateStatement AS Nvarchar(max)
Declare #IdentityField as Nvarchar(max) = (SELECT Name FROM sys.columns WHERE [object_id] = OBJECT_ID(#tableToUpdate) and is_identity = 1)
set #UpdateStatement = 'update ' + #tableToUpdate + ' set ';
Declare #Fields AS Nvarchar(MAX)
SELECT #Fields = COALESCE(#Fields + ', ', '') + #tableToUpdate + '.' + Name + ' = #inserted.' + Name FROM sys.columns WHERE [object_id] = OBJECT_ID(#tableToUpdate) and Name != #IdentityField;
set #UpdateStatement = #UpdateStatement + #fields
set #UpdateStatement = #UpdateStatement + ' FROM ' + #tableToUpdate + ', #inserted'
set #UpdateStatement = #UpdateStatement + ' where ' + #tableToUpdate + '.' + #IdentityField + ' = #inserted.' + #IdentityField
select * into #inserted from inserted
EXECUTE sp_executesql #UpdateStatement
From what I'm finding, it appears that you likely don't have access to use the inserted table in dynamic SQL in this way, but you should be able to accomplish what you're trying to do using the SQLCLR
Using the inserted / deleted tables in Dynamic SQL

SQL Server - Using JSON to return Column Names

I have the following test query I'm needing to make dynamic.
Basically a stored procedure will be passed #json and it needs to return the column names that are passed in that variable.
Is this possible and how could I do it?
declare #json varchar(max)
set #json = '["FirstName", "LastName","DOB"]';
select *
from OPENJSON( #json )
select
FirstName,
LastName,
DOB
from Client
I do have this that works, but not sure on whether it's a good option and whether there's a better way
declare #json varchar(max)
declare #columnames varchar (200)
declare #sqlquery nvarchar(200)
set #json = '["FirstName", "LastName","DOB"]';
set #columnames =''
select #columnames =
case when #columnames = ''
then value
else #columnames + coalesce(',' + value, '')
end
from OPENJSON( #json )
set #sqlquery = 'select ' + #columnames + ' from Client'
EXEC SP_EXECUTESQL #sqlquery
Basically the #json variable can contain one or many or all of the below fields and over-time even more.
set #json = '["FirstName", "LastName","DOB","DrugName,"Age","AgeGroup","Overdose","VerificationCode","Gender"]';
In this case (your json string contain only header with column names) and used for column selection only so it's easy. It's much more harder if you would like get values from json string too.
Variant 1: Use system table to validate your input and get column names.
declare #json varchar(800) = '["FirstName", "LastName", "DOB","DrugName,"Age","AgeGroup", "Overdose","VerificationCode","Gender"]',
#columnames varchar(800)
select #columnames = isnull(#sql + ', ', name) + name
from (
select name, '"' + name + '"' name_quoted
from sys.columns c
where object_id('dbo.Client') = object_id) t
where #json like '%' + name_quoted + '%'
print #columnames
exec ('select ' + #columnames + ' from dbo.Client')
Variant 2: Working with strings.
declare #json varchar(800) = '["FirstName", "LastName","DOB","DrugName,"Age","AgeGroup","Overdose","VerificationCode","Gender"]';
declare #sql varchar(max) = replace(substring(#json, 2, len(#json) - 2), '"','')
print #sql
exec ('select ' + #sql + ' from dbo.Client')

T-SQL string email format issue

I am trying to build an email and have run into an issue. When the stored procedure runs, I get the following error message.
Msg 14624, Level 16, State 1, Procedure sp_send_dbmail, Line 242
At least one of the following parameters must be specified. "#body,
#query, #file_attachments, #subject".
My code is below but I am adding each of the requested items. I have narrowed down where the breakdown happens. If I pull out the concatenation "+" everything works as expected. But I have done this before with the concatenation so I am not sure what is different.
DECLARE #RespPeriod varchar(20)
DECLARE #SubjectLine varchar(100)
DECLARE #ContactEmail varChar(100)
DECLARE #AAEAPVSupplierID int
DECLARE #key varchar(50)
DECLARE #formattedURL varchar(100)
DECLARE #emailBody varchar(max)
DECLARE Curs Cursor
FOR
SELECT theID FROM #temptbl
OPEN Curs
FETCH NEXT FROM Curs INTO #theID
WHILE ##FETCH_STATUS = 0
BEGIN
SELECT *
INTO #temptbl
FROM tblmainTbl
WHERE theID = #theID
DECLARE #isComplete Bit = 1
IF EXISTS (SELECT * FROM #temptbl WHERE Complete = 0)
BEGIN
SET #isComplete = 0
END
IF #isComplete = 1
BEGIN
SET #SubjectLine = 'Testing ' + #RespPeriod + ' Testing.'
SET #ContactEmail = (SELECT SalesEmail FROM #temptbl WHERE theID = #theID)
SET #key = (SELECT ResponseKEY FROM #temptbl WHERE theID = #theID)
SET #formattedURL = 'http://www.something.com/something.aspx?rkey=' + #key
SET #emailBody = '<html>Dear BlaBlaBla' + #RespPeriod + ' ' + #formattedURL + '">' + #formattedURL + '</a></html>'
EXEC msdb.dbo.sp_send_dbmail
#profile_name = 'SMTPProfile'
,#recipients = #ContactEmail
,#subject = #SubjectLine
,#body = #emailBody
,#body_format = 'HTML'
END
DROP TABLE #temptbl
FETCH NEXT FROM Curs INTO #theID
END
CLOSE Curs
DEALLOCATE Curs
Your code sample is incomplete (you're lacking the declaration of some of the variables used). My hunch is one or more of the variable values (maybe #RespPeriod?) is NULL, and when you do the concatenations for the variable assignments used in your sp_send_dbmail call, you're passing NULL.
Remember, string + NULL = NULL
Right before your call to the sp_send_dbmail, insert these statements...
PRINT '--------------'
PRINT '#SubjectLine = ' + ISNULL(#SubjectLine, 'NULL')
PRINT '#ContactEmail = ' + ISNULL(#ContactEmail, 'NULL')
PRINT '#key = ' + ISNULL(#key, 'NULL')
PRINT '#formattedURL = ' + ISNULL(#formattedURL, 'NULL')
PRINT '#emailBody = ' + ISNULL(#emailBody, 'NULL')
PRINT '--------------'
It should quickly become apparent if this is your cause. If it is, chase back the individual parts of whatever variables are resolving as NULL until you find the piece that caused the entire string to be NULL. If it is not, please provide more code so we can look somewhere else.

Resources