sp_executesql with parameters syntax error - sql-server

#id NVARCHAR(12),
#query NVARCHAR(500),
#paramDef NVARCHAR(100) = N'#id NVARCHAR(12)'
I have a syntax error on this line below, specifically at #id
set #query = N'select * from OPENQUERY([REMOTESERVER], ''EXEC db.dbo.dwStoredProc_sp ''#id'')'''
Which I then use
exec sp_executesql #query, #paramDef, #id
I expect my single quotes are incorrect.

Your last ) is placed wrong and you don't need quotes around your #id. Try
set #query =
N'select * from OPENQUERY([REMOTESERVER], ''EXEC db.dbo.dwStoredProc_sp ' + #id + ''')'

You probably don't want to pass '#id' as a string to the SP.
This is likely what you meant:
set #query = N'select * from OPENQUERY([REMOTESERVER], ''EXEC db.dbo.dwStoredProc_sp ' + #id + ''')';

Obviously, it is a quote issue.
But the forum giving you an answer does not give you the tools find a issue in the future.
This is a general debugging issue. Next time, print out the dynamic TSQL. Cut and past into another window.
You will find your issue quickly. Check out the little known processing-instruction command.
Great for huge multiline statements.
-- Old school output to message window
PRINT #query
-- Cool instruction ? Code in new xml tab.
SELECT #query as [processing-instruction(TSQL)] FOR XML PATH
Sample use of print.
You should give Juergen the credit!

Related

Is it possible to issue CREATE statements using sp_executesql with parameters?

I'm trying to dynamically create triggers, but ran into a confusing issue around using sp_executesql and passing parameters into the dynamic SQL. The following simple test case works:
DECLARE #tableName sysname = 'MyTable';
DECLARE #sql nvarchar(max) = N'
CREATE TRIGGER TR_' + #tableName + N' ON ' + #tableName + N' FOR INSERT
AS
BEGIN
PRINT 1
END';
EXEC sp_executesql #sql
However, I want to be able to use #tableName (and other values) as variables within the script, so I passed it along to the sp_executesql call:
DECLARE #tableName sysname = 'ContentItems';
DECLARE #sql nvarchar(max) = N'
CREATE TRIGGER TR_' + #tableName + N' ON ' + #tableName + N' FOR INSERT
AS
BEGIN
PRINT #tableName
END';
EXEC sp_executesql #sql, N'#tableName sysname', #tableName=#tableName
When running the above, I get an error:
Msg 156, Level 15, State 1, Line 2
Incorrect syntax near the keyword 'TRIGGER'.
After trying I few things, I've discovered that even if I don't use #tableName in the dynamic SQL at all, I still get this error. And I also get this error trying to create a PROCEDURE (except, obviously, the message is Incorrect syntax near the keyword 'PROCEDURE'.)
Since the SQL runs fine either directly or when not supplying parameters to sp_executesql, this seems like I'm running into a true limitation in the SQL engine, but I don't see it documented anywhere. Does anyone know if there is a way to accept to a dynamic CREATE script, or at least have insight into the underlying limitation that's being run into?
Update
I can add a PRINT statement, and get the below SQL, which is valid, and runs successfully (when run directly). I still get the error if there's nothing dynamic in the SQL (it's just a single string with no concatenation).
CREATE TRIGGER TR_ContentItems ON ContentItems FOR INSERT
AS
BEGIN
PRINT #tableName
END
I also get the same error whether using sysname or nvarchar(max) for the parameter.
If you execute your create trigger statement that you said you printed... you will find that it does not work. The print statement in the body of the trigger is trying to output #tablename, but is never defined, so you will get an error:
Must declare the scalar variable "#tableName".
But that is not your main issue. As for why you can't seem to execute a DDL statement with execute_sql with parameters, I couldn't find any documentation to explain why... but your experience and others proves that it's troublesome. I believe this post has a pretty good theory: sp_executesql adds statements to executed dynamic script?
You can however execute dynamic sql with DDL statements using the EXECUTE statement. So what you could do is create a parameterized sp_executesql statement that validates your table name and then creates a dynamic sql string to execute with the EXECUTE statement.
It doesn't look pretty, but it works:
DECLARE #tableName sysname = 'MyTable';
DECLARE #sql nvarchar(max) =
N'
set #tableName = (SELECT name FROM sys.tables WHERE OBJECT_ID = OBJECT_ID(#tableName)) --validate table
DECLARE #CreateTriggerSQL as varchar(max) =
''
CREATE TRIGGER '' + QUOTENAME(''TR_'' + #tableName) + '' ON '' + QUOTENAME( #tableName) + '' FOR INSERT
AS
BEGIN
PRINT '''''' + #tableName + ''''''
END
''
print isnull(#CreateTriggerSQL, ''INVALID TABLE'')
exec (#CreateTriggerSQL)
';
EXEC sp_executesql #sql, N'#tableName sysname', #tableName=#tableName;
You could also convert this into a stored procedure with parameters instead of running sp_executesql if that were more convenient. It looks a bit cleaner:
CREATE PROCEDURE sp_AddTriggerToTable (#TableName AS sysname) AS
set #tableName = (SELECT name FROM sys.tables WHERE OBJECT_ID = OBJECT_ID(#tableName)) --validate table
DECLARE #CreateTriggerSQL as varchar(max) =
'
CREATE TRIGGER ' + QUOTENAME('TR_' + #tableName) + ' ON ' + QUOTENAME( #tableName) + ' FOR INSERT
AS
BEGIN
PRINT ''' + #tableName + '''
END
'
print isnull(#CreateTriggerSQL, 'INVALID TABLE')
exec (#CreateTriggerSQL)
GO
I would strongly caution against using Dynamic SQL with table names. You are setting yourself up for some serious SQL Injection issues. You should validate anything that goes into the #tableName variable.
That said, in your example...
DECLARE #tableName sysname = 'ContentItems';
DECLARE #sql nvarchar(max) = N'
CREATE TRIGGER TR_' + #tableName + N' ON ' + #tableName + N' FOR INSERT
AS
BEGIN
PRINT #tableName
END';
EXEC sp_executesql #sql, N'#tableName sysname', #tableName=#tableName
... you are trying to input your declared #tableName into the text you're creating for #sql, and then you're trying to pass a parameter through spexecutesql. This makes your #sql invalid when trying to call it.
You can try:
DECLARE #tableName sysname = 'ContentItems';
DECLARE #sql nvarchar(max) = N'
CREATE TRIGGER TR_'' + #tableName + N'' ON '' + #tableName + N'' FOR INSERT
AS
BEGIN
PRINT #tableName
END';
EXEC sp_executesql #sql, N'#tableName sysname', #tableName=#tableName
... which will give you the string ...
'
CREATE TRIGGER TR_' + #tableName + N' ON ' + #tableName + N' FOR INSERT
AS
BEGIN
PRINT #tableName
END'
... which can then accept the parameter you pass through ...
EXEC sp_executesql #sql, N'#tableName sysname', #tableName=#tableName ;
Again, I'd use some heavy validation (and white-listing) before passing anything into dynamic SQL that will use a dynamic table name.
NOTE: As noted below, I believe you are limited on DML statements that can be executed with sp_executesql(), and I think parameterization is limited also. And based on your other comments, it doesn't sound like you're really needing a dynamic process but a way to repeat a specific task for a handful of elements. If that's the case, my recommendation is to do it manually with a copy/paste then execute the statements.
Since the SQL runs fine either directly or when not supplying
parameters to sp_executesql, this seems like I'm running into a true
limitation in the SQL engine, but I don't see it documented anywhere.
This behavior is documented, albeit not intuitive. The relevant excerpt from the documentation under the trigger limitations topic:
CREATE TRIGGER must be the first statement in the batch
When you execute a parameterized query, the parameter declarations are counted as being part of the batch. Consequently, a CREATE TRIGGER batch (and other CREATE statements for programmability objects like procs, functions, etc.) cannot be executed as a parameterized query.
The invalid syntax error message you get when you attempt to run CREATE TRIGGER as a parameterized query isn't particularly helpful. Below is an simplified version of your code using the undocumented and unsupported internal parameterized query syntax.
EXECUTE(N'(#tableName sysname = N''MyTable'')CREATE TRIGGER TR_MyTable ON dbo.MyTable FOR INSERT AS');
This at least yields an error calling out the CREATE TRIGGER limitation:
Msg 1050, Level 15, State 1, Line 73 This syntax is only allowed for
parameterized queries. Msg 111, Level 15, State 1, Line 73 'CREATE
TRIGGER' must be the first statement in a query batch.
Similarly executing another parameterized statement with this method runs successfully:
EXECUTE (N'(#tableName sysname = N''MyTable'')PRINT #tableName');
But if you don't actually use the parameter in the batch, an error results
EXECUTE (N'(#tableName sysname = N''MyTable'')PRINT ''done''');
Msg 1050, Level 15, State 1, Line 75 This syntax is only allowed for
parameterized queries.
The bottom line is that you need to build the CREATE TRIGGER statement as a string without parameters and execute the statement as a non-parameterized query to create a trigger.
Is it possible to issue CREATE statements using sp_executesql with
parameters?
Simple answer is "No", you can't
According to MSDN
Generally, parameters are valid only in Data Manipulation Language
(DML) statements, and not in Data Definition Language (DDL) statements
You can check more details about this Statement Parameters
What is the issue?
Parameters are only allowed in place of scalar literals, like quoted strings or dates, or numeric values. You can't parameterise a DDL operation.
What can be done?
I believe that you want to use parameterized sp_executesql is to avoid any SQL Injection Attack. To achieve this for the DDL operations you can do following thing to minimize the possibility of attack.
Use Delimiters : You can use QUOTENAME() for SYSNAME parameters like Trigger Name, Table Names and Column names.
Limiting Permissions : User Account you are using to run the dynamic DDL, should have only limited permission. Like on a
specific schema with only CREATE permission.
Hiding Error Message : Don't throw the actual error to the user. SQL Injection are mainly performed by trial and error approach. If
you hide the actual error message, it will become tough to crack it.
Input Validation : You can always have a function which validates the input string, escape the required characters, check
for specific keywords like DROP.
Any workaround?
If you want to parameterized your statement using sp_executesql, in that case you can get the query to be executed in a OUTPUT variable and run the query in next statement like following.
By this, the first call to sp_executesql will parameterized your query, and the actual execution will be performed by the second call to sp_executesql
For example.
DECLARE #TableName VARCHAR(100) = 'MyTable'
DECLARE #returnStatement NVARCHAR(max);
DECLARE #sql1 NVARCHAR(max)=
N'SELECT #returnStatement = ''CREATE TRIGGER TR_''
+ #TableName + '' ON '' + #TableName + '' FOR INSERT AS BEGIN PRINT 1 END'''
EXEC Sp_executesql
#sql1,
N'#returnStatement VARCHAR(MAX) OUTPUT, #TableName VARCHAR(100)',
#returnStatement output,
#TableName
EXEC Sp_executesql #returnStatement
Is it possible to issue CREATE statements using sp_executesql with
parameters?
The answer is "Yes", but with small adjustment:
USE msdb
DECLARE #tableName sysname = 'sysjobsteps';
DECLARE #sql nvarchar(max) = N'
EXECUTE ('' -- Added nested EXECUTE()
CREATE TRIGGER [TR_'' + #tableName + N''] ON ['' + #tableName + N''] FOR INSERT
AS
BEGIN
PRINT '''''+#tableName+'''''
END''
)' -- End of EXECUTE()
EXEC sp_executesql #sql, N'#tableName sysname', #tableName=#tableName
Adjsutments list:
Extra EXECUTE involved, comment below explains why
Extra square brackets added to make SQL Injections slightly harder
I'm looking for specific (ideally, documented) restrictions of
sp_executesql with parameters and if there are any workarounds for
those specific restrictions (beyond not using parameters)
in this case it is a limitation of DDL commands, not sp_executesql. DDL statements cannot be parametrized using variables. Microsoft documentation says:
Variables can be used only in expressions, not in place of object
names or keywords. To construct dynamic SQL statements, use EXECUTE.
source: DECLARE (Transact-SQL)
Therefore, the solution with EXECUTE is provided by me as a workaround
Personally I hate triggers and try to avoid them most of the time ;)
However if you really, really need this dynamic stuff you should use sp_MSforeachtable and avoid injection (as pointed out by Shawn) at any cost:
EXEC sys.sp_MSforeachtable
#command1 = '
DECLARE #sql NVARCHAR(MAX)
SET #sql = CONCAT(''CREATE TRIGGER TR_''
, REPLACE(REPLACE(REPLACE(''?'', ''[dbo].'', ''''),''['',''''),'']'','''')
, '' ON ? FOR INSERT
AS
BEGIN
PRINT ''''?'''';
END;'');
EXEC sp_executesql #sql;'
, #whereand = ' AND object_id IN (SELECT object_id FROM sys.objects
WHERE name LIKE ''%ContentItems%'')';
If you want to use the parameter as string, add double ' before and after the parameter name
like this :
DECLARE #tableName sysname = 'ContentItems';
DECLARE #sql nvarchar(max) = N'
CREATE TRIGGER TR_' + #tableName + N' ON ' + #tableName + N' FOR INSERT
AS
BEGIN
print ''' + #tableName
+''' END';
EXEC sp_executesql #sql
And if you want to use it as table name, use select instead of print ,
like this :
DECLARE #tableName sysname = 'ContentItems';
DECLARE #sql nvarchar(max) = N'
CREATE TRIGGER TR_' + #tableName + N' ON ' + #tableName + N' FOR INSERT
AS
BEGIN
select * from ' + #tableName
+' END';
EXEC sp_executesql #sql

OPENROWSET: sp_executesql statement failing at #param

I am developing a program to pull in the XML file that WordPress enables you to download (essentially a backup copy).
At this point I was automating the process to allow frequent backup of my data on SQL Server, and for some reason I am stuck at developing the query to run the OPENROWSET where the XML file will be located.
DECLARE #SQL NVARCHAR(MAX)
DECLARE #ParamDefinition NVARCHAR(500) = N'#fstring NVARCHAR(MAX)'
DECLARE #string VARCHAR(MAX) =
N'C:\[FilePath]\Reviews\thehesperian2016-07-29.xml'
SET #SQL =
N'INSERT INTO #Temp (Extract_Date, XMLDATA)
SELECT GETDATE()
, A.*
FROM OPENROWSET(BULK #fstring, SINGLE_BLOB, CODEPAGE = ' + '''RAW''' + ') AS A'
EXEC sp_executesql #SQL
, #ParamDefinition
, #fstring = #string
The error:
Msg 102, Level 15, State 1, Line 4
Incorrect syntax near '#fstring'.
I can turn this into a simple query on a table in the predicate, so I have reason to suspect it is the way the filepath is read.
I've spent a few hours racking my brain trying to figure out why this is wrong. While I COULD use QUOTENAME as in this example in the BULKINSERT, I was hoping to embed all of that in the dynamic SQL (thus still use sp_executesql)
What or why am I doing this wrong? Any help will be greatly appreciated.
- Regards,
ANSWER
OPENROWSET - MSDN declares in its own paragraph:
OPENROWSET does not accept variables for its arguments.
QUOTENAME is sufficient, although I did run a few minor REPLACEfunctions anyways.
The data file path the OPENROWSET function does not allow a parameter. Instead, build the needed string with the literal:
DECLARE #string varchar(MAX) = N'C:\[FilePath]\Reviews\thehesperian2016-07-29.xml';
DECLARE #SQL nvarchar(MAX);
SET #SQL =
N'INSERT INTO #Temp (Extract_Date, XMLDATA)
SELECT GETDATE()
, A.*
FROM OPENROWSET(BULK ' + QUOTENAME(#string, '''') + ', SINGLE_BLOB, CODEPAGE = ''RAW'') AS A';
EXEC sp_execute #SQL;
--EXECUTE(#SQL);
UPDATE:
Added QUOTENAME in case the provided file path is from an untrusted source. Also, note that OPENROWSET queries are not autoparameterized. It makes no difference whether one executes the query with sp_executesql or EXECUTE here.

Replace whole table by a parameter?

Is it possible to replace a whole table by a parameter?
I would like to write a statement which looks like this:
declare #tab as nvarchar(100);
set #tab = 'dbo.tblCostCenters';
select * from #tab;
However, this does not work.
So the only other way I know so far is to create dynamic SQL. But this then is subject to SQL Injections. Is there possibly a better solution ?
Yes, you can do it. It is a bad idea.
DECLARE #sql nvarchar(max)
SELECT #sql =
'SELECT * FROM '
+ ISNULL(QUOTENAME(PARSENAME(#tab,2))+'.','')
+ QUOTENAME(PARSENAME(#tab,1))
WHERE OBJECT_ID(#tab,'U') IS NOT NULL
EXEC sp_executesql #sql

Want to insert values from one table to another table but from two different databases

My problem is the following
insert into TargetDatabase.dbo.tblContact
Select * from SourceDatabase.dbo.tblContact
As shown above. I want to insert data of same table into same table but database is different
I tried the following
Create Procedure Demo
#SourceDatabase as nvarchar(100),
#TargetDatabase as nvarchar(100)
as
exec ( 'insert into' +#TargetDatabase+'.dbo.tblContact')
exec('select * from ' +#SourceDatabase+'.dbo.tblContact')
In this code Select Query is working Fine
but while inserting it is throwing error 'Incorrect syntax near tbl Contact.'
You're running two different exec statements.
Do it in one single one:
exec ('insert into' + #TargetDatabase + '.dbo.tblContact ' +
'select * from ' + #SourceDatabase+'.dbo.tblContact')
Just a slightly different approach. I prefer sp_executesql over EXEC (some background here) and I find REPLACE a little cleaner than classic concatenation, especially when a variable is embedded in the script multiple times. I also typically add a #debug flag so I can optionally print the statement for sanity checking instead of executing it.
CREATE PROCEDURE dbo.Demo
#SourceDatabase NVARCHAR(100),
#TargetDatabase NVARCHAR(100),
#debug BIT = 0
AS
BEGIN
DECLARE #sql NVARCHAR(MAX);
SET #sql = N'INSERT [$t$].dbo.tblContact SELECT * FROM [$s$].dbo.tblContact;';
SET #sql = REPLACE(REPLACE(#sql, '$t$', #TargetDatabase), '$s$', #SourceDatabase);
IF #debug = 1
PRINT #sql;
IF #debug = 0
EXEC sp_executesql #sql;
END
GO
I will caution against using INSERT with no column list and SELECT * - this code is very brittle as a change to either table will result in errors, wrong data, or worse.

Creating stored procedure in another database

Any idea if it's possible to create a procedure in another database using T-SQL alone, where the name of the database is not known up front and has to be read from a table? Kind of like this example:
Use [MasterDatabase]
Declare #FirstDatabase nvarchar(100)
Select Top 1 #FirstDatabase=[ChildDatabase] From [ChildDatabases]
Declare #SQL nvarchar(4000)
Declare #CRLF nvarchar(10) Set #CRLF=nchar(13)+nchar(10)
Set #SQL =
'Use [+'#Firstdatabase+']'+#CRLF+
'Go'+#CRLF+
'Create Proc [Test] As Select 123'
Exec (#SQL)
See what I'm trying to do? This example fails because Go is actually not a T-SQL command but it something recognised by the query analyser/SQL management studio and produces an error. Remove the Go and it also fails because Create Proc must be the first line of the script. Arrgg!!
The syntax of T-SQL doesn't allow you do things like this:
Create [OtherDatabase].[dbo].[Test]
Which is a shame as it would work a treat! You can do that with Select statements, shame it's inconsistent:
Select * From [OtherDatabase]..[TheTable]
Cheers, Rob.
It's a pain, but this is what I do. I took this from an example I found on sqlteam, I think - you might have some quoting issues with the way I did the indiscriminate REPLACE:
DECLARE #sql AS varchar(MAX)
DECLARE #metasql as varchar(MAX)
DECLARE #PrintQuery AS bit
DECLARE #ExecQuery AS bit
SET #PrintQuery = 1
SET #ExecQuery = 0
SET #sql =
'
CREATE PROCEDURE etc.
AS
BEGIN
END
'
SET #metasql = '
USE OtherDatabase
EXEC (''' + REPLACE(#sql, '''', '''''') + ''')
'
IF #PrintQuery = 1
PRINT #metasql
IF #ExecQuery = 1
EXEC (#metasql)
DECLARE #UseAndExecStatment nvarchar(4000),
#SQLString nvarchar(4000)
SET #UseAndExecStatment = 'use ' + #DBName +' exec sp_executesql #SQLString'
SET #SQLString = N'CREATE Procedure [Test] As Select 123'
EXEC sp_executesql #UseAndExecStatment,
N'#SQLString nvarchar(4000)', #SQLString=#SQLString
This is how i have done with Alter Procedure:
DECLARE #metasql as varchar(MAX)
DECLARE #sql AS varchar(MAX)
SET #sql =
'ALTER PROCEDURE [dbo].[GetVersion]
AS
BEGIN
SET NOCOUNT ON;
SELECT TOP(1)[Version] from VersionTable
END'
SET #metasql = '
USE MyProdDb
IF (OBJECT_ID(''GetVersion'') IS NOT NULL OR OBJECT_ID(''GetVersion'', ''P'') IS NOT NULL)
BEGIN
EXEC (''' + REPLACE(#sql, '''', '''''') + ''')
END
'
--PRINT #metasql
EXEC (#metasql)
You could shell out to osql using xp_cmdshell, I suppose.
I dont think this can be done with TSQL.
You could use an SSIS package that looped the names and connected to the servers dynamically which creates the schema (procs ) you need.
This is probably what I would do as it means it is all contained within the package.
Configuration can be kept separate by either using a table or external xml file that contained the list of server/databases to deploy the schema to.
It's not necessary to use EXEC within EXEC.
You can simply use OtherDatabase.sys.sp_executesql
DECLARE #sql AS varchar(MAX) = N'
CREATE PROCEDURE etc.
AS
BEGIN
-- whatever
END
';
PRINT #sql;
EXEC OtherDatabase.sys.sp_executesql #sql;

Resources