-1

Note: My intention with this question was to learn about approaches other than mine, to execute multiple CREATE TRIGGER statements in a single string. Not "solving" this, by executing one by one, is not the requirement. No need to use temporary tables, while loops, cursors, etc. I hope someone else can benefit from this post, although it has had a strangely negative and unfortunate reception.

I want to dynamically create many triggers in the same script, but it fails, do you know any way to achieve this?

Try the following script:

declare @commands varchar(max) = '
    drop table if exists dbo.Countries
    drop table if exists dbo.Cities

    create table dbo.Countries(
        id int identity not null primary key,
        Country varchar(50) not null
    )

    create table dbo.Cities(
        id int identity not null primary key,
        City varchar(50) not null
    )
'
print @commands

/* This execution works well */
execute (@commands)
go

declare @commands varchar(max) = '
    create trigger tr_Countries on
        dbo.Countries for insert as
    begin
        print ''A new country was created.''
    end

    create trigger tr_Cities on
        dbo.Cities for insert as
    begin
        print ''A new city was created.''
    end
'
print @commands

/* This execution fails:
Msg 156, Level 15, State 1, Procedure tr_Countries, Line 8 [Batch Start Line 20]
Incorrect syntax near the keyword 'trigger'. 
*/
execute (@commands)

As you can see, the two batches are very similar, but the first one runs successfully and the second one fails.

The idea is to apply this pattern to metadata maintenance:

  • Select the objects to be processed
  • Create commands over those objects with a query
  • Compile the commands list into a string
  • Execute dynamically the commands string
declare @Commands varchar(max)

/*  You get a list of objects that meet your requirements,
      normally it will be a query with multiple tables and conditions */
;with QueryObjectsYouNeed as (
    select 'Table_01' TableName union all
    select 'Table_02' TableName union all
    select 'Table_03' 
),
/* With those objects create the commands */
CommandsList as (
    select 'drop table if exists ' + TableName Command
        from QueryObjectsYouNeed
)
/* Compile the commands into a string */
select @Commands = string_agg(cast(Command as varchar(max)), char(13))
    from CommandsList

print @Commands
/* Execute dynamically the commands string */
Execute(@Commands)

Another way is to use a table variable or a temporary table, store the commands in it, and then iterate the list to execute each command one at a time.

13
  • The error is not very helpful, but you apparently can't create 2 triggers in one batch. Commented Apr 30, 2024 at 3:19
  • Yes, the intention is to create multiple DDL commands with one query and execute them all at once. Commented Apr 30, 2024 at 3:24
  • 3
    @FcoJavier99 yeah my comment is saying you can't do that for some commands e.g. Create Trigger that require their own batch. Commented Apr 30, 2024 at 3:27
  • 2
    you CANNOT put create second trigger in same script. you have to generate a new string Commented Apr 30, 2024 at 3:36
  • 2
    Just execute each trigger separately Commented Apr 30, 2024 at 5:24

3 Answers 3

-1
+100

Try below solution (I tested with success):

Instead of run this (at last):

execute (@commands)

Run below proc to go around limit:

EXEC dbo.execute_MULTI_command @commands;

Definition of SQL proc dbo.execute_MULTI_command:

CREATE or alter PROC dbo.execute_MULTI_command 
@commands varchar(max) = '
    go
    create trigger tr_Countries on
        dbo.Countries for insert as
    begin
        print ''A new country was created.''
    end
    go
    create trigger tr_Cities on
        dbo.Cities for insert as
    begin
        print ''A new city was created.''
    end
'
as

BEGIN
declare @search_string varchar(64)='create trigger';


declare @i_begin int = 0;
declare @i_end   int = 0;
declare @each_command varchar(max) = NULL;

while @i_begin < len(@commands)
    begin
        set @i_end=CHARINDEX(@search_string, @commands, @i_begin+1)
        --print @i_begin;
        --print @i_end;
        if @i_end=0
          break;
        select @each_command = SUBSTRING(@commands,@i_begin,@i_end-@i_begin);
        --print @each_command;
        execute (@each_command);
        select @i_begin = @i_end
    end
declare @last_command varchar(max)=SUBSTRING(@commands,@i_begin,len(@commands)-@i_begin);
--print @last_command;
execute (@last_command);

END
Sign up to request clarification or add additional context in comments.

5 Comments

Ok, you are running each 'create trigger' separately, I prefer in that case to build and run each one, instead of creating a single string and split it after that.
Great idea! I would improve this by splitting commands by "GO" keywords instead of hard coding specific types of objects.
@Alex Splitting by "GO" is tricky... There might be cases of "go" not as batch separator....
ZXBUCKS, read GO is not a statement. This is not the prefered approach for me, but it would work if done properly. I'd suggest to create your own very distinct batch separator and then use STRING_SPLIT to get the chunks for execution.
@ZXBUCKS Please read the note at the beginning of the question, The requirement was not met really, but thanks for share your solution 👍🏻, I appreciate your good vibes.
-1

The whole approach has some smell...

Why?

  1. Well, triggers have a small always. But there are cases... Try to avoid them...
  2. There are many mature approaches to achieve something like this, just to name a few:
  • Database projects in Visual Studio
    (allow for a clean "master", will create diff scripts to any sibling)
  • EF-Migrations (if your work may follow code-first principles)
  • Database compare tools
    (various free and commercial tools create diff scripts between versions of databases).

It looks like you trying to re-invent such tools...

However, some statements are not possible to be called within the the same batch. Some tell you something like "must be the first command", others, like CREATE TRIGGER, are very picky in many ways (e.g. cannot be applied to another database than the one in USE and don't allow USE otherDB in dynamically created statements.)

What can you do?

  1. Try to re-think your approach and find existing tools for the same.
  2. Create a separated script and use a SP like the other answer to execute as a one-liner.
    Hint: I would not use GO, but something more distinct, like #$%mySep%$# (read GO is not a statement)
  3. Use your own batch execution with separated commands like the following example (one of the rare cases where I use a CURSOR):

-- a test db just not to ruin anything in your system

CREATE DATABASE dummyShnugo;
GO
USE dummyShnugo;
GO

--creating your command stack (ID IDENTITY helps with the sorting)

DECLARE @tbl TABLE(ID INT IDENTITY, cmd NVARCHAR(MAX));
INSERT INTO @tbl(cmd) VALUES(N'CREATE TABLE Test(ID INT IDENTITY, SomeText NVARCHAR(100), SomeDate DATETIME2);');
INSERT INTO @tbl(cmd) VALUES(N'
CREATE TRIGGER tr_Test ON Test FOR INSERT 
AS
BEGIN
    UPDATE Test SET SomeDate = SYSUTCDATETIME() WHERE ID IN(SELECT inserted.ID FROM inserted); --yeah, no need for this, DEFAULT does the same.
END');
INSERT INTO @tbl(cmd) VALUES(N'INSERT INTO Test(SomeText) VALUES(N''abc''),(N''def'');');
INSERT INTO @tbl(cmd) VALUES(N'SELECT * FROM Test;');

-- CURSOR iteration for execution (you might use PRINT instead of EXEC)

DECLARE @cmd NVARCHAR(MAX);
DECLARE cur CURSOR FOR SELECT cmd FROM @tbl ORDER BY ID;
OPEN cur
FETCH NEXT FROM cur INTO @cmd;

WHILE @@FETCH_STATUS=0
BEGIN
    --PRINT @cmd;
    EXEC (@cmd);
    FETCH NEXT FROM cur INTO @cmd;
END
CLOSE cur;
DEALLOCATE cur;
GO

--Clean up

USE master;
GO
DROP DATABASE dummyShnugo;

Comments

-2

The way I found (on my own) is to emulate the 'go' batches by nesting 'execute()' commands in the string to execute.

Then I can apply this amazing pattern (IMHO) to metadata maintenance:

  • Select the objects to be processed
  • Create commands over those objects with a query
  • Compile the commands list into a string
  • Execute dynamically the commands string
/* Clean up code */
drop table if exists geo.Continents
drop table if exists geo.Countries
drop table if exists geo.Cities
drop schema if exists geo
go

/* Setup code */
create schema geo
create table geo.Continents(
    id int identity not null primary key,
    Continent varchar(50) not null
)
create table geo.Countries(
    id int identity not null primary key,
    Country varchar(50) not null
)
create table geo.Cities(
    id int identity not null primary key,
    City varchar(50) not null
)
go

/* Generate the create trigger commands and execute them */
declare @Commands varchar(max)
declare @command varchar(max) = '
execute(''    
    create or alter trigger geo.tr_%s_i 
        on geo.%s for insert 
    as
        print ''''A new rew(s) was(were) created in the table: %s.''''
'')'
declare @geo_schema_id int = schema_id('geo')

;with GeoTables as (
    /* Get the 'geo' schema tables */
    select name
        from sys.tables
        where schema_id=@geo_schema_id 
)
,CommandsList as (
    select formatmessage(@command, name, name, name)  Command
        from GeoTables
)
select @Commands = string_agg(cast(Command as varchar(max)), char(13))
    from CommandsList

print @Commands
/* Execute dynamically the commands string */
Execute(@Commands)

5 Comments

Be careful when wrapping exec stuff, what happens when table name contains spaces or quotes, you might wanna look into QUOTENAME
Yes, in my work the naming convention is to never use spaces between the name or special characters, only underline to separate parts of the name, but yes, in general if you work with third party code it is better to use QUOTENAME.
@FcoJavier99, dynamically created sql is tricky, nested dynamically sql is very tricky :). One hint: CREATE TRIGGER will always affect the database you are using in the execution context. You will not be allowed to create a trigger with fully qualified name or switch context with USE).
@Shnugo, trigger schema is optional when you create it, please execute the code, and review the Microsoft's documentation: learn.microsoft.com/en-us/sql/t-sql/statements/…. This code is executing in the current database but if you change the database context in the script, it works fine. Please shared some tricky situation that are you referring.
@FcoJavier99, I did not mean the schema (which is optional if targeting the default) but a different database. You can write create table dbname.schema.tablename(...) but you cannot do the same for a trigger. Btw: was it you downing my answer? If so, mind to explain?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.