Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 488
bdukes Avatar asked Jan 30 '19 17:01

bdukes


People also ask

Can we use Sp_executesql in function?

It appears that you can't. You can execute extended stored procedure inside a function and, even though sp_executesql is an extended stored procedure (despite its name), it still generates the message "only functions and extended stored procedures can be executed within a function".

What is the use of Sp_executesql?

sp_executesql can be used instead of stored procedures to execute a Transact-SQL statement many times when the change in parameter values to the statement is the only variation.

What is the difference between execute and Sp_executesql?

sp_executesql supports parameterisation, whereas EXEC only accepts a string. Only performance differences that may arise are due to the parameterisation i.e. a parameterised sp_executesql call is more likely to have a reusable cached plan.


2 Answers

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
like image 181
Brian Pressler Avatar answered Oct 05 '22 17:10

Brian Pressler


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.

like image 33
Dan Guzman Avatar answered Oct 05 '22 16:10

Dan Guzman