如何在使用 exec 创建存储过程时关闭带引号的标识符

How to set quoted identifier off while creating stored procedure using exec

我有一个方案可以通过编程方式更改少数用户定义的 table 类型的列。为此,我需要删除引用的存储过程。因此,我设计了 SQL 脚本来执行以下活动:

  1. 将 sys.sql_modules table 中的存储过程备份到暂存 table(定义,uses_ansi_nulls 和 uses_quoted_identifier 列)。
  2. 正在删除存储过程。
  3. 改变用户定义的 table 类型。
  4. 现在,使用暂存 table 的定义列重新创建存储过程,但我无法使用 uses_ansi_nulls 和 uses_quoted_identifier 列的值。 如何在重新创建存储过程时重用 uses_ansi_nulls 和 uses_quoted_identifier 列。

我正在使用游标将暂存列的内容放入变量中,然后使用 exec() 执行存储过程的定义,如下所示:

 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)

以上语句报错:

 Incorrect syntax near 'GO'. 

当我删除 GO 语句时,它给出了错误:

'CREATE/ALTER PROCEDURE' must be the first statement in a query batch.

在动态语句之外定义设置。例如,如果您更改外部作用域的 ANSI_NULLS 设置,动态语句的作用域也将继承该设置。

例如:

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

因为后两个语句都 return 32 这意味着 ANSI_NULLS 在两个语句中都启用了。同样,0 表示它在前面的语句中被禁用。

请注意,如果您更改内部(动态)范围内的设置,此设置更改不会传播到外部范围:

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

至于为什么GO出错,那是因为不是一个T-SQL操作符,所以不被识别。 GO 是实用程序语句,被 IDE 和 CLI(例如 SSMS、ADS 和 sqlcmd)识别为批处理分隔符。它不被 T-SQL 编译器识别,因此不应包含在动态语句中。

你不能做你想做的事。不是通过使用游标遍历 table 中的行并将定义拉出到变量中来执行并根据每行给出的值更改 ansi_nullsquoted_identifier,因为游标必须分批执行。

为什么“必须住一批”很重要?继续阅读。

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

这将打印 256。到目前为止,一切都很好。但是这个呢?

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

这会先打印 256 然后再打印 0 吗?没有。它打印 0,然后再次打印 0。诡异的!好的,让我们确保 quoted_identifier 是由 运行 在其自己的批次中 on,然后尝试有条件地关闭它:

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

当我们开始第二批(go 之后的位)时,quoted_identifier 设置肯定是打开的。那我们只关掉它if 1 = 0。由于 1 不等于 0,因此 quoted_identifier 应该保留。所以我们希望打印 256.

我们实际打印的是什么? 0。这是怎么回事?让我们检查一下 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.

(强调)

您不能有条件地更改批处理中的 quoted_identifier 设置。似乎动态 SQL 也救不了你,因为你需要 set quoted_identifier 然后 create procedure 在同一个动态 sql 字符串中,你不能,因为一个动态 exec 是一个批次,而 create procedure 必须是批次中的第一个语句。

等等,还有更多。

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);

为了清楚发生了什么,这是输出:

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

所以...耶!我们已经设法执行了一个动态 sql 的片段(@both 的值),并且我们已经更改了其中 quoted_identifier 的设置!凉爽的。我们付出了什么代价? exec().

的嵌套动态调用

这能拯救我们吗?没有。

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

exec 的嵌套调用对我们没有帮助,因为(来自文档):

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.

你可以做什么?

运行 您的“光标”在程序创建批处理之外

例如,编写一个小程序(或 powershell 脚本)读取备份定义以及所需的设置,然后将每个 create procedure 作为自己的命令执行。

或者读取备份的内容并将其全部转储到一个文件中,包括备份中每一行的“go”语句table。将文件内容作为脚本执行。

基于程序的解决方案的伪代码:

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