-1

I have a scenario to alter the columns of few user defined table types programmatically. To do this, I need to drop the referenced stored procedures. So, I have designed my SQL scripts to do following activities:

  1. Taking the backup of stored procedures from sys.sql_modules table into a staging table (definition, uses_ansi_nulls and uses_quoted_identifier columns).
  2. Dropping the stored procedure.
  3. Altering the user defined table types.
  4. Now, using the staging table's definition column to recreate the stored procedure, but I am not able to use the values of uses_ansi_nulls and uses_quoted_identifier columns. How can I reuse the uses_ansi_nulls and uses_quoted_identifier column while recreating the stored procedure.

I am using cursor to get the content of staging columns into variables and then using exec() executing the definition of stored procedure like below:

 SET @ProcDefinition_Complete=@uses_ansi_nulls_Definition + CHAR(10)+' GO '+ CHAR(10)+@uses_quoted_identifier_Definition+ CHAR(10)+' GO '+  CHAR(10)+ @ProcDefinition
        
        EXEC (@ReferencingDefinition_Complete)

Above statement gives error:

 Incorrect syntax near 'GO'. 

And when I remove GO statement then it gives the error:

'CREATE/ALTER PROCEDURE' must be the first statement in a query batch.
  • 2
    Well I would probably focusing on changing those objects to not _have_ or _require_ either of those settings to be `OFF`. – Aaron Bertrand Sep 03 '21 at 11:00
  • 2
    Adding the @AaronBertrand 's comment, both options must be on to use features such as filtered indexes and indexed views. In my experience, they are most often off because of inattention to detail when the object was initially created. – Dan Guzman Sep 03 '21 at 11:05

2 Answers2

3

You can't do what you're trying to do. Not by iterating over the rows in the table with a cursor and pulling the definitions out into variables to execute and changing ansi_nulls and quoted_identifier according to the values given by each row, because the cursor execution has to live in one batch.

Why does "have to live in one batch" matter? Read on.

set quoted_identifier on;
select @@options & 256; -- will print 256 if qi is on, 0 if it is off

This will print 256. So far so good. But what about this?

set quoted_identifier on;
print @@options & 256;
set quoted_identifier off;
print @@options & 256;

Does this print 256 and then 0? Nope. it prints 0, and the 0 again. Weird! OK, let's make sure quoted_identifier is on by running that in its own batch, and then try turning it off conditionally:

set quoted_identifier on;
go -- note this additional go
if (1 = 0) set quoted_identifier off;
print @@options & 256;

When we start the second batch (the bit after go) the quoted_identifier settings is most certainly on. Then we only turn it off if 1 = 0. Since 1 does not equal 0, quoted_identifier should stay on. So we would expect to print 256.

What do we actually print? 0. What's going on? Let's check the docs:

For a top-level ad-hoc batch parsing begins using the session's current setting for QUOTED_IDENTIFIER. As the batch is parsed any occurrence of SET QUOTED_IDENTIFIER will change the parsing behavior from that point on, and save that setting for the session. So after the batch is parsed and executed, the session's QUOTED_IDENTIFER setting will be set according to the last occurrence of SET QUOTED_IDENTIFIER in the batch.

(Emphasis added)

You can't conditionally change the quoted_identifier setting within a batch. It seems like dynamic SQL can't save you either, because you would need to set quoted_identifier and then create procedure in the same dynamic sql string, and you can't, because one dynamic exec is one batch, and create procedure has to be the first statement in the batch.

But wait, there's more.

declare @cmdOn varchar(max) = 'exec(''set quoted_identifier on; print @@options & 256;'')';
declare @cmdOff varchar(max) = 'exec(''set quoted_identifier off; print @@options & 256;'');';

declare @both varchar(max) = concat(@cmdOn, char(10), @cmdOff);
print @both;
exec (@both);

Just so it's clear what's going on, here's the output of that:

exec('set quoted_identifier on; print @@options & 256;')
exec('set quoted_identifier off; print @@options & 256;');
256
0

So... yay! We've managed to execute a single piece of dynamic sql (the value of @both), and we've changed the setting of quoted_identifier inside that! Cool. What did it cost us? Nested dynamic calls to exec().

Does this save us? Nope.

set quoted_identifier on; -- this is the only line that matters;

declare @qi varchar(max) = 'exec(''set quoted_identifier off;'')'; -- it makes no difference what you put here
declare @def varchar(max) = 'exec(''create or alter procedure p as begin set nocount on end;'');';

declare @both varchar(max) = concat(@qi, char(10), @def);
exec (@both);

select uses_quoted_identifier from sys.sql_modules where object_name(object_id) = 'p';

-- returns 1

The nested calls to exec don't help us, because (from the docs):

For a nested batch using sp_executesql or exec(), the parsing begins using the QUOTED_IDENTIFIER setting of the session. If the nested batch is inside a stored procedure, parsing starts using the QUOTED_IDENTIFIER setting of the stored procedure. As the nested batch is parsed, any occurrence of SET QUOTED_IDENTIFIER will change the parsing behavior from that point on, but the session's QUOTED_IDENTIFIER setting will not be updated.

What can you do instead?

Run your "cursor" outside the procedure creation batch.

For instance, write a small program (or powershell script) which reads the backup definitions, along with the required settings, and then executes each create procedure as its own command.

Or read the contents of the backups and dump it all to a file, including "go" statements for each row in the backups table. The execute the file contents as a script.

Pseudocode for the program based solution:

take backup of sql_modules via ssms/whatever
drop procedures via ssms/whatever
run program
   open connection to sql
   read definition, settings from backup table
   foreach (definition, settings)
      execute sql command ("set ansi_nulls ?; set quoted_identifier ?");
      execute sql command (definition)
   close connection
exit program
 
allmhuran
  • 4,154
  • 1
  • 8
  • 27
  • Can you please elaborate this method a little bit: For instance, write a small program (or powershell script) which reads the backup definitions, along with the required settings, and then executes each create procedure as its own command. – Murali Dhar Darshan Sep 04 '21 at 09:08
  • @MuraliDharDarshan I have added pseudocode for the process – allmhuran Sep 04 '21 at 09:27
  • 1
    Thank you for your help. This is working in powershell: $SqlCmd = New-Object System.Data.SqlClient.SqlCommand $SqlCmd.Connection=New-Object System.Data.SqlClient.SqlConnection $SqlCmd.Connection.ConnectionString='Server=Server1;Database=TestSP;Integrated Security=True' $SqlCmd.Connection.Open() $SqlCmd.CommandText="SET ANSI_NULLS OFF;SET QUOTED_IDENTIFIER ON;" $SqlCmd.ExecuteNonQuery() $SqlCmd.CommandText="CREATE PROC abc AS BEGIN SELECT 1 END" $SqlCmd.ExecuteNonQuery() $SqlCmd.Connection.Close() – Murali Dhar Darshan Sep 06 '21 at 05:29
1

Define the setting outside of the dynamic statement. If you change the outer scope's ANSI_NULLS setting, for example, the dynamic statement's scope will inherit that setting too.

For example:

SET ANSI_NULLS OFF;
SELECT @@OPTIONS & 32; --Returns 0
EXEC sys.sp_executesql N'SELECT @@OPTIONS & 32;'; --Returns 0
GO

SET ANSI_NULLS ON;
SELECT @@OPTIONS & 32; --Returns 32
EXEC sys.sp_executesql N'SELECT @@OPTIONS & 32;'; --Returns 32

As the latter 2 statements both return 32 this means that ANSI_NULLS is enabled in both statements. Likewise, 0 means it is disabled in the former statements.

Note that if you change the setting within the inner (dynamic) scope, this setting change is not propagated to the outer scope:

SELECT @@OPTIONS & 32; --returns 32
EXEC sys.sp_executesql N'SET ANSI_NULLS OFF; SELECT @@OPTIONS & 32;'; --returns 0
SELECT @@OPTIONS & 32; --Returns 32

As for why GO errors, that's because it's not a T-SQL operator, and thus isn't recognised. GO is a Utility Statement, and is recognised by IDEs and CLIs (such as SSMS, ADS, and sqlcmd) as a batch separator. It is not recognised by the T-SQL compiler, and thus should not be included in dynamic statements.

Thom A
  • 88,727
  • 11
  • 45
  • 75
  • How I can recreate the SP because it would still give error that Create Proc should be the first statement, if I put set ansi_null on/off as the first statement in the same scope? – Murali Dhar Darshan Sep 03 '21 at 11:38
  • As I stated, @MuraliDharDarshan , you *don't* put it in the same scope... – Thom A Sep 03 '21 at 11:41
  • Below statement is not working... it's not inheriting the ANSI_NULLS and QUOTED_IDENTIFIER settings for SP: EXEC sys.sp_executesql N'SET ANSI_NULLS OFF;' EXEC sys.sp_executesql N'SET QUOTED_IDENTIFIER OFF'; EXEC sys.sp_executesql @ReferencingDefinition – Murali Dhar Darshan Sep 03 '21 at 11:59
  • ...With respect, @MuraliDharDarshan , did you actually read my answer...? I ***explicitly*** state that **won't** work... From the very answer above *"Note that if you change the setting within the inner (dynamic) scope, **this setting change is not propagated to the outer scope**"* You're changing the setting within an inner scope, and then expecting it to effect the outer scope, which then affects a later separate inner scope... The inner scope's settings *do not* effect the outer scope's settings. – Thom A Sep 03 '21 at 12:05
  • Ok got it. **In the same scope, not possible** because it will give error that Create procedure should be the first statement. And in this case **different scope, not possible** because its dynamic query. – Murali Dhar Darshan Sep 03 '21 at 12:27
  • *"and in this case different scope, not possible because its dynamic query"* That's not what I say in the answer either, @MuraliDharDarshan ... "Define the setting outside the dynamic statement"... The **outer** scope's settings are used in the *inner* scope. I clearly demonstrate this above. – Thom A Sep 03 '21 at 12:29