T-SQL Newline String Concatenation For Exec() - sql

So, I've come across a pretty annoying problem with T-SQL... Essentially, in a table, there are several T-SQL statements. I want a stored procedure to efficiently grab these rows and concatenate them to a variable. Then, execute the concatenated lines with EXEC(#TSQL)
The problem is, string concatenation with newlines seem to be stripped when calling Exec...
For example something like:
declare #sql nvarchar(max) = ''
select #sql += char(13) + char(10) + [sql] from [SqlTable]
exec(#sql) -- Won't always do the right thing
print #sql -- Looks good
I don't want to butcher the code with a Cursor, is there any way around this? Thanks!
Edit:
Okay, so it looks like the issue is in fact only with the GO statement, for example:
declare #test nvarchar(max) = 'create table #test4(i int) ' + char(10) + char(13) + 'GO' + char(10) + char(13) +'create table #test5(i int)'
exec(#test)
I guess this go will have to go (no pun intended) I just really didn't want to have to try and parse it in fear of special cases blowing up the whole thing.

A select statement without order by is free to return results in any order.
You'd have to specify the order in which your SQL snippets make sense:
select #sql += char(13) + char(10) + [sql]
from [SqlTable]
order by
SnippetSequenceNr
As #Bort suggested in the comments, if the snippets are stand-alone SQL, you can separate them with a semicolon. Carriage returns, newlines, tabs and spaces are all the same in T-SQL: they're whitespace.
select #sql += [sql] + ';'
from [SqlTable]
order by
SnippetSequenceNr

Just get rid of the GO "statements". As noted by others you also might need to ensure the string is constructing in the correct statement sequence. Using += is probably not the best idea, though I'm not sure about the dynamic sql idea in the first place. It might actually be more appropriate to use a cursor here.

Sam F,
Your method didn't work with FOR XML method of string concatenation (if you wanted to create a "line" delimited list, based on values found in different rows of a table). However, replace the char(13) with SPACE(13), then it works great.
SELECT PackageNote = SUBSTRING((SELECT (SPACE(13) + char(10) + PackageDescription)
FROM POPackageNote PN2
WHERE PN1.PurchaseOrderNumber = PN2.PurchaseOrderNumber
ORDER BY POPackageNoteID, PackageDescription
FOR XML PATH( '' )
), 3, 1000 )
FROM POPackageNote PN1
WHERE (PurchaseOrderNumber = #PurchaseOrderNumber)
GROUP BY PurchaseOrderNumber

Related

Find rows containing delimited words within nvarchar parameter

I have a procedure that selects an offset of rows from a table:
SELECT * --table contains ID and Name columns
FROM Names
ORDER BY ID
OFFSET #Start ROWS
FETCH NEXT #Length ROWS ONLY
In addition to #Start and #Length parameters, the procedure also receives #SearchValue NVARCHAR(255) parameter. #SearchValue contains a string of values delimited by a space, for example '1 ik mi' or 'Li 3'.
What I need is to query every record containing all of those values. So, if the #SearchValue is '1 ik mi', it should return any records that contain all three values: '1', 'mi', and 'ik'. Another way to understand this is by going here, searching the table (try searching 00 eer 7), and observing the filtered results.
I have the freedom to change the delimiter or run some function (in C#, in my case) that could format an array of those words.
Below are our FAILED attempts (we didn't try implementing it with OFFSET yet):
Select ID, Name
From Names
Where Cast(ID as nvarchar(255)) in (Select value from string_split(#SearchValue, ' ')) AND
Name in (Select value from string_split(#SearchValue, ' '))
SELECT ID, Name
FROM Names
WHERE #SearchValueLIKE '% ' + CAST(ID AS nvarchar(20)) + ' %' AND
#SearchValueLIKE '% ' + Name + ' %';
We used Microsoft docs on string_split for the ideas above.
Tomorrow, I will try to implement this solution, but I'm wondering if there's another way to do this in case that one doesn't work. Thank you!
Your best bet will be to use a FULL TEXT index. This is what they're built for.
Having said that you can work around it.. BUT! You're going to be building a query to do it. You can either build the query in C# and fire it at the database, or build it in the database. However, you're never going to be able to optimise the query very well because users being users could fire all sorts of garbage into your search that you'll need to watch out for, which is obviously a topic for another discussion.
The solution below makes use of sp_executesql, so you're going to have to watch out for SQL injection (before someone else picks apart this whole answer just to point out SQL injection):
DROP TABLE #Cities;
CREATE TABLE #Cities(id INTEGER IDENTITY PRIMARY KEY, [Name] VARCHAR(100));
INSERT INTO #Cities ([Name]) VALUES
('Cooktown'),
('South Suzanne'),
('Newcastle'),
('Leeds'),
('Podunk'),
('Udaipur'),
('Delhi'),
('Murmansk');
DECLARE #SearchValue VARCHAR(20) = 'ur an rm';
DECLARE #query NVARCHAR(1000);
SELECT #query = COALESCE(#query + '%'' AND [Name] LIKE ''%', '') + value
FROM (Select value from string_split(#SearchValue, ' ')) a;
SELECT #query = 'SELECT * FROM #Cities WHERE [Name] LIKE ''%' + #query + '%''';
EXEC sp_executesql #query;

Change dynamic SQL from using Exec to sp_executesql

I am a beginner in SQL and esspecially dynamic sql execution.
I am trying to convert some dynamic sql which is inside a store procedure from using EXEC to using sp_executesql.
The issue I have is I don't know what are the exact rules for representing multiple lines of dynamic sql. The existing code is in the form of:
Set #cmd = 'WITH vdate AS' + char(13)
+' (SELECT valueID,' + char(13)
+'username)' +char(13)
+'FROM dbo.table1' + char(13)
+'WHERE username = ''' + convert(varchar(11), #username, 106) + ''' AND' + char(13)
Exec (#cmd)
The above is just a snippet of what it looks like. There are alot more lines and more complicated stuff going on.
I now want to be able to use the sp_executesql way of executing this code because I want to change the username attribute to be a table which accepts multiple usernames.
Please can you advise me what the syntax is for this and how do i make the line before last work?
I have seen that code looks very similar so what i did I changed the +' to be N' at the start of each line and the code compiled but it didn't execute when I tried to use the query in my application.
Thanks, Jetnor.
The only difference is that instead of EXEC (#cmd) you would use EXEC sp_executesql #cmd. Technically speaking, sp_executesql takes NVARCHAR arguments. In order to create an NVARCHAR string you should prefix the string with N like so:
DECLARE #cmd NVARCHAR(1000);
SET #cmd = N'My string'
This denotes that the string is a Unicode string which may contain national character sets. Of course you can omit the N but if you have anything other than ASCII characters in it then it won't work properly and you'll end up with strings that contain a lot of question marks instead of your special characters (CJK - Chinese, Japanese, Korean - character sets for instance) so it is good practice to always prefix NVARCHAR strings with the N.
Some of your confusion arises because you are replacing the string concatenation operator, +, with the national character set identifier, N. You will need both. It is always worth including proper spacing in your code, especially in dynamic SQL because it's easier to read. My preference is to add the spaces at the end of lines where possible, but that's just my personal preference. Also, drop the CHAR(13) stuff unless you really want to print it out in a certain way. Your example would become:
DECLARE #cmd NVARCHAR(1000) --Note this is NVARCHAR, not VARCHAR.
SET #cmd = N'WITH vdate AS '
+ N'(SELECT valueID, '
+ N'username) '
+ N'FROM dbo.table1 '
+ N'WHERE username = ''' + convert(NVARCHAR(11), #username, 106) + ''' AND ' --Note that you are missing a chunk here - AND what?
EXEC sp_executesql #cmd
Note that your CONVERT is incorrect: Firstly you should convert to NVARCHAR (if #username isn't already), and secondly the 106 part does nothing - that's for date formatting. Also you are missing the end of the statement as there is an AND and then nothing. It's always good to use PRINT #cmd to see if the SQL you've generated is valid (it will output the contents of #cmd and you can then copy that and paste it into a workshet in SSMS).
Now, you might want to have output parameters in your dynamic SQL as well which is no problem.
DECLARE #ID INT;
DECLARE #outputValue NVARCHAR(50);
DECLARE #cmd NVARCHAR(1000);
SET #cmd = N'SELECT #value = Value FROM MyTable WHERE ID = ' +
CAST(#ID AS NVARCHAR(5));
EXEC sp_executesql #cmd, '#value NVARCHAR(50) OUT', #outputValue OUT;
SELECT #outputValue;
Traditionally, the main reason for wanting to use sp_executesql as opposed to just EXEC is that you have a better chance of the execution plan being cached and therefore re-used later. I confess I'm not sure if that's still true or not, but sp_executesql is generally the preferred method of executing dynamic SQL. A lot of poeple have an instinctive hatred of dynamic SQL but frankly, like all things, it has it's uses so long as it isn't abused and used where it just isn't required.

Checking whether conditions are met by all rows with dynamic SQL

I have a table in SQL Server 2008 which contains custom validation criteria in the form of expressions stored as text, e.g.
StagingTableID CustomValidation
----------------------------------
1 LEN([mobile])<=30
3 [Internal/External] IN ('Internal','External')
3 ([Internal/External] <> 'Internal') OR (LEN([Contact Name])<=100)
...
I am interested in determining whether all rows in a table pass the conditional statement. For this purpose I am writing a validation stored procedure which checks whether all values in a given field in a given table meet the given condition(s). SQL is not my forte, so after reading this questions this is my first stab at the problem:
EXEC sp_executesql N'SELECT #passed = 0 WHERE EXISTS (' +
N'SELECT * FROM (' +
N'SELECT CASE WHEN ' + #CustomValidationExpr + N' THEN 1 ' +
N'ELSE 0 END AS ConditionalTest ' +
N'FROM ' + #StagingTableName +
N')t ' +
N'WHERE t.ConditionalTest = 0)'
,N'#passed BIT OUTPUT'
,#passed = #PassedCustomValidation OUTPUT
However, I'm not sure if the nested queries can be re-written as one, or if there is an entirely better way for testing for validity of all rows in this scenario?
Thanks in advance!
You should be able to reduce by at least one subquery like this:
EXEC sp_executesql N'SELECT #passed = 0 WHERE EXISTS (' +
N'SELECT 1 FROM ' + #StagingTableName +
N'WHERE NOT(' + #CustomValidationExpr + N')) ' +
,N'#passed BIT OUTPUT'
,#passed = #PassedcustomValidation OUTPUT
Before we answer the original question, have you looked into implementing constraints? This will prevent bad data from entering your database in the first place. Or is the point that these must be dynamically set in the application?
ALTER TABLE StagingTable
WITH CHECK ADD CONSTRAINT [StagingTable$MobileValidLength]
CHECK (LEN([mobile])<=30)
GO
ALTER TABLE StagingTable
WITH CHECK ADD CONSTRAINT [StagingTable$InternalExternalValid]
CHECK ([Internal/External] IN ('Internal','External'))
GO
--etc...
You need to concatenate the expressions together. I agree with #PinnyM that a where clause is easier for full table validation. However, the next question will be how to identify which rows fail which tests. I'll wait for you to ask that question before answering it (ask it as a separate question and not as an edit to this one).
To create the where clause, something like this:
declare #WhereClause nvarchar(max);
select #WhereClause = (select CustomValidation+' and '
from Validations v
for xml path ('')
) + '1=1'
select #WhereClause = replace(replace(#WhereClause, '<', '<'), '>', '>'))
This strange construct, with the for xml path('') and the double select, is the most convenient way to concatenate values in SQL Server.
Also, put together your query before doing the sp_executesql call. It gives you more flexibilty:
declare #sql nvarchar(max);
select #sql = '
select #passed = count(*)
from '+#StagingTableName+'
where '+#WhereClause
That is the number that pass all validation tests. The where clause for the fails is:
declare #WhereClause nvarchar(max);
select #WhereClause = (select 'not '+CustomValidation+' or '
from Validations v
for xml path ('')
) + '1=0'

Why do I get these different results from two SQL queries?

This has been killing me all day =D. Please help!
Scenario 1: Two DB's on the same server (A, B) and A has three tables. Query is executed from B.
Scenario 2: Two DB's one server is linked to the other. (B has link A) and A has three tables. Query is executed from B.
In scenario 1 (non linked server):
SET #val = ''
SELECT #val = #val + 'Hello, my name is ' + [name] + '!' + CHAR(10) + CHAR(13)
FROM A.sys.tables
Returns:
Hello, my name is Table1!
Hello, my name is Table2!
Hello, my name is Table3!
In scenario 2 (linked server):
SET #val = ''
SELECT #val = #val + 'Hello, my name is ' + [name] + '!' + CHAR(10) + CHAR(13)
FROM LINKED.A.sys.tables
Returns:
Hello, my name is Table3!
Why are these different? If I use openquery() on the linked server the results are the same as scenario 1. I'm trying to avoid using openquery() if possible. Thanks!
Unfortunately, this is an unreliable method of string concatenation in SQL Server. I would avoid it in all but the most trivial of cases. There is some more information in this KB: Execution Plan and Results of Aggregate Concatenation Queries Depend Upon Expression Location.
That said, I was able to both duplicate your problem and provide a workaround in my environment:
SET #val = ''
SELECT #val = #val + 'Hello, my name is ' + replace([name], '', '') + '!' + CHAR(10) + CHAR(13)
FROM LINKED.A.sys.tables
Notice that I've added an empty replace function to the expression. Though it should do nothing to the output, it does add a local "compute scalar" step to the query plan. This seems to pull back all of the data from the name column to then be processed locally rather than just letting the remote query return what it thinks is needed.
I'm not sure if there's a better function to use other than a replace with empty arguments. Perhaps a double reverse or something. Just be sure to cast to a max datatype if necessary as the documentation states.
UPDATE
Simply declaring #var as varchar(max) rather than nvarchar(max) clears up the problem, as it then brings back the entire name column (type sysname -- or nvarchar(128) -- I believe) for local processing just like the replace function did. I cannot pretend to know which combination of linked server settings and implicit casting causes this to come up. Hopefully someone with more knowledge in this area can chime-in!

Creating a long string from a result set

I have a result set in MS-SQL within a stored procedure, and lets say it has one VARCHAR column, but many rows. I want to create a comma separated string conataining all these values, Is there an easy way of doing this, or am I going to have to step through each result and build the string up manually?
Preferably I'd like to do this in the Stored Procedure itself.
Here is one way (using AdventureWorks2008 DB):
DECLARE #name varchar(255)
SET #name = NULL
select #Name = COALESCE(#Name + ',','') + LastName from Person.Person
Select #name
And here is another (for SQL 2005 onwards):
SELECT
LastName + ','
FROM
Person.Person
FOR XML PATH('')
In both cases you will need to remove the trailing comma ',' (can use STUFF() function)