Trying to use SQL Replace in a non-dynamic way - sql

I have the following statement, where I'm passing in a parameter like this:
'0001,0003'
I was following the REPLACE answer from this question:
SQL IN Statement splitting parameter
But I'm trying to take it out of dynamic sql. My returned result is NULL. Is there anyway to get this to work?
DECLARE #partialLNum varchar(MAX)
DECLARE #lNumConCat varchar(500)
DECLARE #tTemp table(lNum varchar(15))
DECLARE #formatIN varchar(MAX)
set #partialLNum = '0001,0003'
set #formatIN = ''''+ REPLACE(#partialLNum,',',''',''')+''''
insert into #tTemp
select substring(lNum,1,2) + '-' + substring(lNum,3,3) + '-' + substring(lNum,6,2) + '-' + substring(lNum,8,3)
from [rpt].[myView]
where LNum IN (#formatIN)
select #lNumConCat = COALESCE(#lNumConCat +'' , '', '''') + LNum from #tTemp
select #lNumConCat

in takes a list of values. So:
where LNum IN ('0001,0003')
has a list with one element, that happens to have a comment in it.
One way to do what you want is using like:
where ',' + partialLNum + ',' like '%,' + LNum + ',%'
There should suffice, but there are other ways using a split() function as well.

Related

SQL : Getting Incorrect Syntax near '=' While using CASE statement with variables

I am trying to perform the below query where some variables are being used. This below SQL code is a part of a stored procedure. Idea is to dynamically set the target columns and its values based on FileKey.
DECLARE #TargetColNames nvarchar(max) = '';
DECLARE #SourceColNames nvarchar(max) = '';
DECLARE #SourceColNamesInsert nvarchar(max) = '';
DECLARE #UpdateColumns nvarchar(max) = '';
SELECT
CASE
WHEN #FileKey IN ('s_1','s_2')
THEN #TargetColNames = #TargetColNames + ' [CreatedUser], [UpdatedUser], [CreatedDateTime],[UpdatedDateTime],[IsDeleted],[DeletedOn]'
ELSE #TargetColNames = #TargetColNames + ' [CreatedUser], [UpdatedUser], [CreatedDateTime], [UpdatedDateTime]'
END,
#SourceColNames = CONCAT('CreatedUser','UpdatedUser','CreatedDateTime', 'UpdatedDateTime'),
#SourceColNamesInsert = CONCAT(''',#User, ''',''',#User, ''', 'Getdate()', 'Getdate()' ),
CASE
WHEN #FileKey IN ('s_1','s_2')
THEN #UpdateColumns = CONCAT('Target.UpdatedUser= ''',#User,''', 'Target.[IsDeleted]=0','Target.[DeletedOn]=null')
ELSE #UpdateColumns = CONCAT('Target.UpdatedUser= ''',#User,''', 'Target.UpdatedDateTime=Getdate()')
END
The above SQL statement throws an error:
Msg 102, Level 15, State 1, Procedure uspDynamicStageToPropLayer1, Line 165 [Batch Start Line 5]
Incorrect syntax near '='.
What am I missing here? Maybe this is quite a silly mistake...
Also, by doing concat with your quoted parts, you might allow SQL injection into your query even building dynamically. What if a user's name (or forced parameter has a leading single quote, then garbage injection such as
#User = ';drop table X --
Having said that, some of the stuff could be more simplified, such as
SELECT
#TargetColNames = #TargetColNames
+ ' [CreatedUser], [UpdatedUser], [CreatedDateTime], [UpdatedDateTime]'
+ CASE WHEN #FileKey IN ('s_1','s_2')
THEN ', [IsDeleted], [DeletedOn]'
else ''
end
For the insert, you will probably get a failure. If you look at a possible result of the #User
#SourceColNamesInsert = concat(''',#User, ''',''',#User, ''',
'Getdate()', 'Getdate()' ),
will result with the value below which is not what I think is intended. Notice no comma's between values because the triple ' is creating start and end literals, and leaves no actual comma between column insert values.
',#User, '',#User, 'Getdate()Getdate()
But instead...
select concat('''#User'', ''#User''', ', Getdate(), Getdate()' );
which will result in...
'#User', '#User', Getdate(), Getdate()
The ''' actually creates an opening quoted string immediately with the value after it, then the '' (double) closes the quoted string, but it also adds the comma separator before the next '' (double) to start second user and ''' (triple) to close the second #User, then adding comma and both getdate() calls.
'#User', '#User', Getdate(), Getdate()
Now, if the value for #User was 'Bob', and your intent was to have the string output as
'Bob', 'Bob', Getdate(), Getdate()
change to
select concat('''', #User, ''', ','''', #User, '''', ', Getdate(), Getdate()' );
The '''' (quad) means I want to open a string, do a single quote (by the inner two ''), and close this as its own string. then get the value of the #User. Then follow by an open string ' with '' for closing the quote around the name, then open a single quote to start the next, the comma before starting the next quote for the second user and closing it as well via '''', then the value of the user again, and finally closing the second user with close quote ''''. Finally adding a comma and getdate() calls. Yes, stupid tricky in the quoting.
An easier implementation without CONCAT() is just using + between each explicit part such as
select '''' + #User + '''' + ', ' + '''' + #User + '''' + ', Getdate(), getdate()' ;
where each '''' is a single quote thus resulting in
' + Bob + ' + , + ' + Bob + ' + , Getdate(), getdate()
resulting in
'Bob', 'Bob', Getdate(), getdate()
I'll leave the final UpdateColumns to you to confirm your intended output.
But, as mentioned, beware of possible SQL injection when you are dynamically building SQL statements with embedded parameter values as this appears to be doing.
Here is another way to approach this:
Declare #FileKey varchar(10) = 's_3'
, #TargetColNames nvarchar(max) = ''
, #SourceColNames nvarchar(max) = ''
, #SourceColNamesInsert nvarchar(max) = ''
, #UpdateColumns nvarchar(max) = '';
Set #TargetColNames = concat_ws(', ', '[CreatedUser]', '[UpdatedUser]', '[CreatedDateTime]', '[UpdatedDateTime]');
If #FileKey In ('s_1', 's_2')
Set #TargetColNames = concat_ws(', ', #TargetColNames, '[UpdatedDateTime]', '[IsDeleted]', '[DeletedOn]');
Print #TargetColNames;
Set #SourceColNames = concat_ws(', ', '[CreatedUser]', '[UpdatedUser]', '[CreatedDateTime]', '[UpdatedDateTime]');
Set #SourceColNamesInsert = concat_ws(', ', quotename('#User', char(39)), quotename('Getdate()', char(39)), quotename('Getdate()', char(39)));
Print #SourceColNames;
Print #SourceColNamesInsert;

How to convert charater string to uniqueidentifier and use Stuff function?

I'm using function to generate the result. What am I facing now is I pass the ItemGuid as parameter and currently I am using STUFF to find the ItemCode and concatenate result. However I getting an error said that Conversion failed when converting from a character string to uniqueidentifier.
My current result before using STUFF:
From Date : 01-01-2021 to 31-03-2021 Item No: IN ('a70014a3-2e00-41f0-9c3e-6fb8c4f2ab60','26dd67c1-fe37-41fa-b8c5-ff033928a291')
My expected result:
From Date : 01-01-2021 to 31-03-2021 Item No: IN ('ITM001','ITM021')
Please see my fiddle. SQL Fiddle
Parameter used: SELECT[dbo].[func_ReportCriteria2]('2021-01-01','2021-03-31','''a70014a3-2e00-41f0-9c3e-6fb8c4f2ab60'',''26dd67c1-fe37-41fa-b8c5-ff033928a291''') AS 'RESULT 2'
--using STUFF
CREATE FUNCTION [dbo].[func_ReportCriteria2]
(#FromDate DateTime
,#ToDate DateTime
,#Item_List NVARCHAR(MAX)
)
RETURNS nvarchar(max)
AS
BEGIN
DECLARE #CRITERIA NVARCHAR(MAX)
DECLARE #sqlCommand NVARCHAR(MAX)
DECLARE #ItemResult NVARCHAR(MAX)
SET #sqlCommand = ''
IF(ISNULL(#Item_List,'') != '')
BEGIN
--find ItemCode and concatenate based on ItemGuid
--error occur here
SET #ItemResult = STUFF( (SELECT ',' + ItemCode
FROM Item
WHERE ItemGuid IN (#Item_List)
FOR XML PATH('')), 1, 1, '')
SET #sqlCommand = 'Item No: IN ('+ #ItemResult +') '
END
SET #CRITERIA = 'From Date : ' + CONVERT(NVARCHAR(19),#FromDate,105) + ' to ' + CONVERT(NVARCHAR(19),#ToDate,105)
+ CHAR(13)+CHAR(10) + #sqlCommand
RETURN #CRITERIA
END
Please check if this fit your need:
declare #FromDate CHAR(10) ,#ToDate CHAR(10), #Item_List nvarchar(MAX)
select
#FromDate = '2021-01-01',-- make sure to convert the DATE from the table to NVARCHAR using style 120
#ToDate = '2021-03-31',-- make sure to convert the DATE from the table to NVARCHAR using style 120
#Item_List = '''a70014a3-2e00-41f0-9c3e-6fb8c4f2ab60'',''26dd67c1-fe37-41fa-b8c5-ff033928a291'''
------------------ Solution --------------------
-- Important! No reason for scalar function! Use it inline your query directly
SELECT
N'From Date : ' + #FromDate + ' to ' + #ToDate + N' Item No: IN (' + STRING_AGG(''''+ItemName+'''',',') + N')'
FROM Item
WHERE ItemGuid in (
-- Your string includes quotes which we must clear befor CONVERT to GUIDE
SELECT REPLACE([value],'''','') FROM STRING_SPLIT(#Item_List, ',')
)
Note! concat input text to string might be NOT safe and NOT recommended. It has a potential to SQL Injection!
Note: You probably plan a dynamic query, which will SELECT data from the table using the above string, which you asked to build. In this case, this seems like "XY problem" since you probably do not need to build this string at all. You can use the same approach to split string input and execute a direct SELECT query from your table. If we knew what is your final requested result is, then we could help in this as well, but the approach is the same is above code
Try apply convert itemguid
Change WHERE ItemGuid IN (#ItemList)
To WHERE cast(ItemGuid as NVARCHAR(MAX)) IN (#ItemList)

Remove only leading or trailing carriage returns

I'm dumbfounded that this question has not been asked meaningfully already. How does one go about creating an equivalent function in SQL like LTRIM or RTRIM for carriage returns and line feeds ONLY at the start or end of a string.
Obviously REPLACE(REPLACE(#MyString,char(10),''),char(13),'') removes ALL carriage returns and new line feeds. Which is NOT what I'm looking for. I just want to remove leading or trailing ones.
Find the first character that is not CHAR(13) or CHAR(10) and subtract its position from the string's length.
LTRIM()
SELECT RIGHT(#MyString,LEN(#MyString)-PATINDEX('%[^'+CHAR(13)+CHAR(10)+']%',#MyString)+1)
RTRIM()
SELECT LEFT(#MyString,LEN(#MyString)-PATINDEX('%[^'+CHAR(13)+CHAR(10)+']%',REVERSE(#MyString))+1)
Following functions are enhanced types of trim functions you can use. Copied from sqlauthority.com
These functions remove trailing spaces, leading spaces, white space, tabs, carriage returns, line feeds etc.
Trim Left
CREATE FUNCTION dbo.LTrimX(#str VARCHAR(MAX)) RETURNS VARCHAR(MAX)
AS
BEGIN
DECLARE #trimchars VARCHAR(10)
SET #trimchars = CHAR(9)+CHAR(10)+CHAR(13)+CHAR(32)
IF #str LIKE '[' + #trimchars + ']%' SET #str = SUBSTRING(#str, PATINDEX('%[^' + #trimchars + ']%', #str), LEN(#str))
RETURN #str
END
Trim Right
CREATE FUNCTION dbo.RTrimX(#str VARCHAR(MAX)) RETURNS VARCHAR(MAX)
AS
BEGIN
DECLARE #trimchars VARCHAR(10)
SET #trimchars = CHAR(9)+CHAR(10)+CHAR(13)+CHAR(32)
IF #str LIKE '%[' + #trimchars + ']'
SET #str = REVERSE(dbo.LTrimX(REVERSE(#str)))
RETURN #str
END
Trim both Left and Right
CREATE FUNCTION dbo.TrimX(#str VARCHAR(MAX)) RETURNS VARCHAR(MAX)
AS
BEGIN
RETURN dbo.LTrimX(dbo.RTrimX(#str))
END
Using function
SELECT dbo.TRIMX(#MyString)
If you do use these functions you might also consider changing from varchar to nvarchar to support more encodings.
In SQL Server 2017 you can use the TRIM function to remove specific characters from beginning and end, in one go:
WITH testdata(str) AS (
SELECT CHAR(13) + CHAR(10) + ' test ' + CHAR(13) + CHAR(10)
)
SELECT
str,
TRIM(CHAR(13) + CHAR(10) + CHAR(9) + ' ' FROM str) AS [trim cr/lf/tab/space],
TRIM(CHAR(13) + CHAR(10) FROM str) AS [trim cr/lf],
TRIM(' ' FROM str) AS [trim space]
FROM testdata
Result:
Note that the last example (trim space) does nothing as expected since the spaces are in the middle.
Here's an example you may run:
I decided to cast the results as an Xml value, so when you click on it, you will be able to view the Carriage Returns.
DECLARE #CRLF Char(2) = (CHAR(0x0D) + CHAR(0x0A))
DECLARE #String VarChar(MAX) = #CRLF + #CRLF + ' Hello' + #CRLF + 'World ' + #CRLF + #CRLF
--Unmodified String:
SELECT CAST(#String as Xml)[Unmodified]
--Remove Trailing Whitespace (including Spaces).
SELECT CAST(LEFT(#String, LEN(REPLACE(#String, #CRLF, ' '))) as Xml)[RemoveTrailingWhitespace]
--Remove Leading Whitespace (including Spaces).
SELECT CAST(RIGHT(#String, LEN(REVERSE(REPLACE(#String, #CRLF, ' ')))) as Xml)[RemoveLeadingWhitespace]
--Remove Leading & Trailing Whitespace (including Spaces).
SELECT CAST(SUBSTRING(#String, LEN(REPLACE(#String, ' ', '_')) - LEN(REVERSE(REPLACE(#String, #CRLF, ' '))) + 1, LEN(LTRIM(RTRIM(REPLACE(#String, #CRLF, ' '))))) as Xml)[RemoveAllWhitespace]
--Remove Only Leading and Trailing CR/LF's (while still preserving all other Whitespace - including Spaces). - 04/06/2016 - MCR.
SELECT CAST(SUBSTRING(#String, PATINDEX('%[^'+CHAR(13)+CHAR(10)+']%',#String), LEN(REPLACE(#String, ' ', '_')) - PATINDEX('%[^'+CHAR(13)+CHAR(10)+']%',#String) + 1 - PATINDEX('%[^'+CHAR(13)+CHAR(10)+']%', REVERSE(#String)) + 1) as Xml)[RemoveLeadingAndTrailingCRLFsOnly]
Remember to remove the Cast-to-Xml, as this was done just as a Proof-of-Concept to show it works.
How is this better than the currently Accepted Answer?
At first glance this may appear to use more Functions than the Accepted Answer.
However, this is not the case.
If you combine both approaches listed in the Accepted Answer (to remove both Trailing and Leading whitespace), you will either have to make two passes updating the Record, or copy all of one Logic into the other (everywhere #String is listed), which would cause way more function calls and become even more difficult to read.
I was stuck using Microsoft SQL Server 2008 R2 and so basing my functions on #sqluser's answer I came up with the below. This will return an empty string if the string only contains the characters to be trimmed.
The bit that threw me was the pattern for PATINDEX must be included between % characters, which for a while I was thinking of as the same wildcard in a LIKE statement but which I now believe is just the syntax to denote a pattern, though I may be wrong!
CREATE FUNCTION [dbo].[ExtendedLTRIM](#string_to_trim VARCHAR(MAX))
RETURNS VARCHAR(MAX)
AS
BEGIN
DECLARE #tab CHAR(1) = CHAR(9);
DECLARE #line_feed CHAR(1) = CHAR(10);
DECLARE #carriage_return CHAR(1) = CHAR(13);
DECLARE #space CHAR(1) = CHAR(32);
DECLARE #characters_to_trim VARCHAR(10)
SET #characters_to_trim = #tab + #line_feed + #carriage_return + #space
IF #string_to_trim LIKE '[' + #characters_to_trim + ']%'
BEGIN
DECLARE #first_non_trim_character INT = PATINDEX('%[^' + #characters_to_trim + ']%', #string_to_trim);
IF #first_non_trim_character = 0 RETURN '';
RETURN SUBSTRING(#string_to_trim, #first_non_trim_character, 8000)
END
RETURN #string_to_trim
END
GO
To trim characters from a pre-defined list you'll want to create the following UDF (should work in 2008R2 and above).
Handles both sides in a single pass and doesn't care if it's a CRLF, LFCR (yep, seen that abomination more than once), bare LF or a bunch of spaces.
is easy to extend to e.g. add additional parameters to do LTRIM/RTRIM only, or a full purge (that last bit is simpler to do in 2017 by incorporating STRING_AGG, but perfectly doable in 2008R2); as a matter of fact this is a simplified version of something I use to do all those things. If anybody is interested then let me know and I'll update:
CREATE FUNCTION fnTrimHarder
(
#String VARCHAR(MAX)
)
RETURNS VARCHAR(MAX)
AS
BEGIN
DECLARE
#Start INT,
#Len INT,
#Chars CHAR(5) = CONCAT(
CHAR(9), -- TAB
CHAR(10), -- LF
CHAR(13), -- CR
' '
), -- List of invalid characters
#Return VARCHAR(MAX) = '';
IF #String NOT LIKE '%[^' + #Chars + ']%' -- If string contains only invalid characters
OR COALESCE(#String, '') = '' -- Optional addition for NULL handling
RETURN #Return
ELSE
BEGIN -- Create a "table" of characters with ordinals, calculate the start of string and its length, then return the substring
WITH CTE AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1
FROM CTE
WHERE n < LEN(#String)
)
SELECT
#Start = MIN(n),
#Len = 1 + MAX(n) - MIN(n)
FROM CTE
WHERE SUBSTRING(#String, n, 1) NOT LIKE '[' + #Chars + ']';
SET #Return = SUBSTRING(#String, #Start, #Len)
END
RETURN #Return
END
GO

Single quote in SQL WHERE IN statement

Okay so my T-SQL statement is the following
IF #dept <> '' BEGIN
SET #query = #query + ' AND T6.Region2 IN (' + REPLACE(#dept, '^', '''') + ') '
END
This is basically for a French site and the region names have ' in the names. What I'm trying to do is pass something like the following to SQL
#dept = 'Cote-d^Armor', 'Val-d^Oise'
and replace the ^ with a '. When I do a replace it's just causing an error as if the replaced ^ is a ' and escaping the rest of the code.
Hope that makes sense.
You need some extra quotes outside the replace. Try this:
IF #dept <> '' BEGIN
SET #query = #query + ' AND T6.Region2 IN (''' + REPLACE(#dept, '^', '''') + ''') '
END
I wouldn't swap "'" for "^" to store it and then try and convert it back. It would be much simpler to store "Cote-d'Armor", then in your stored procedure whenever there is a "'" in the search string, to double it.
SQL Server can happily cope with ' and " in the data, they just need to be escaped properly.

A more elegant way of escaping dynamic SQL?

OK, so I've got this line of code in a search stored procedure:
SET #where = 'job_code = ''' + REPLACE(#job_code, '''', '''''') + ''''
and there are basically two operations I'd like to streamline -the first being surrounding the concatenated value in single quotes. Obviously, in the above statement, I'm escaping a ' by using two '' and then ending the string with a ' so I can concatenate the actual value. There's got to be a better way!
The second of the operations would be the REPLACE(#job_code, '''', '''''') where I'm escaping any single quotes that might exist in the field.
Isn't there a much more elegant way of writing this line of code as a whole?
I thought it was the ESCAPE keyword but that's tied tightly to the LIKE statement, so no go there.
Not sure how you execute your sql query, if you use sp_executesql, could be something like this
EXECUTE sp_executesql
N'SELECT * FROM YouTable WHERE job_code = #job_code',
N'#job_code varchar(100)',
#job_code = #job_code;
The parameterized query answer is probably the real "right answer", but to answer your original question, what you want is QUOTENAME(). More specifically, the single-quote version:
SET #where = 'job_code = ' + QUOTENAME(#job_code, '''')
Do note the length limit on this (input is a sysname, meaning 128 characters), though, as it is intended to quote the names of database objects and not as a general-purpose mechanism.
You could declare constants:
declare #SQ as char(1) = ''''
SET #where = 'job_code = ' + #SQ + REPLACE(#job_code, #SQ, #SQ + #SQ) + #SQ
You could define a function that handles your typical scenarios, something like:
create function WrapAndReplaceQuotes (#input as varchar(max))
returns varchar(max)
as
begin
return '''' + replace(#input, '''', '''''') + ''''
end
SET #where = 'job_code = ' + WrapAndReplaceQuotes(#job_code)