从数百个存储过程中解析出完整的动态 SQL 表达式

Parsing out complete dynamic SQL expressions from hundreds of stored procedures

我继承了一个大型应用程序,它有数百个存储过程,其中许多使用动态 SQL。为了更好地处理我正在处理的 SQL 类型,如果我有办法解析 all 这些存储的查询文本,那将非常有用程序并提取包含在其中的任何动态 SQL 的完整表达式。

一个简化的表达式可能是:

declare @query nvarchar(max)
set @query = 'SELECT col1,col2,col3 from ' + @DatabaseName + '.dbo.' + @TableName + ' WHERE {some criteria expression that also contains inline quotes}'

我正在寻找的上述输出(最终将在解析所有存储过程的单个查询中调用)是:

SELECT col1, col2, col3 
FROM ' + @DatabaseName + '.dbo.' + @TableName + ' 
WHERE {some criteria expression that also contains inline quotes}

所以,不是传入参数值后的表达式,而是存储过程文本中的表达式文本,包括参数名称。

我同意动态 SQL 参数名称为 @query 的不安全假设,因此在 SQL 表达式中搜索它以使用作为提取文本的起始位置是可以容忍的,但由于内联有单引号,我没有简单的方法知道变量的赋值在哪里完成。

我在这个问题中加入了 [antlr] 和 [parsing] 标签,因为我觉得这超出了 T-SQL.

的能力范围

PS:是的,我很清楚"I shouldn't be doing this"。

编辑

根据下面的建议,尝试了以下查询但在这种情况下并不是很有用:

SELECT 
db_name(dbid) DB_NAME
,cacheobjtype, objtype, object_name(objectid) ObjectName
,objectid 
,x.text
,usecounts 
--  , x.*,z.* ,db_name(dbid)
FROM 
sys.dm_exec_cached_plans z
CROSS APPLY sys.dm_exec_sql_text(plan_handle)  x
WHERE 
    --usecounts > 1 
    --objType = 'Proc' and  -- include if you only want to see stored procedures 
    db_name(dbid) not like 'ReportServer%' and db_name(dbid) <> 'msdb' and db_name(dbid) not like 'DBADB%' and db_name(dbid) <> 'master'
--ORDER BY usecounts DESC
ORDER BY objtype

初步估计,下面是使用 ScriptDom 在 C# 中执行此操作的方法。

获取所有存储过程定义的列表很容易。这可以在 T-SQL 中完成,甚至:

sp_msforeachdb 'select definition from [?].sys.sql_modules'

或者以通常的方式编写脚本数据库,或者使用 SMO。无论如何,我假设您可以通过某种方式将它们放入 List<string> 中,供代码使用。

Microsoft.SqlServer.TransactSql.ScriptDom is available as a NuGet 包,因此将其添加到全新的应用程序中。 我们问题的核心是编写一个访问者,它将从 T-SQL 脚本中提取我们感兴趣的节点:

class DynamicQueryFinder : TSqlFragmentVisitor {
  public List<ScalarExpression> QueryAssignments { get; } = new List<ScalarExpression>();
  public string ProcedureName { get; private set; }

  // Grab "CREATE PROCEDURE ..." nodes
  public override void Visit(CreateProcedureStatement node) {
    ProcedureName = node.ProcedureReference.Name.BaseIdentifier.Value;
  }

  // Grab "SELECT @Query = ..." nodes
  public override void Visit(SelectSetVariable node) {
    if ("@Query".Equals(node.Variable.Name, StringComparison.OrdinalIgnoreCase)) {
      QueryAssignments.Add(node.Expression);
    }
  }

  // Grab "SET @Query = ..." nodes
  public override void Visit(SetVariableStatement node) {
    if ("@Query".Equals(node.Variable.Name, StringComparison.OrdinalIgnoreCase)) {
      QueryAssignments.Add(node.Expression);
    }
  }

  // Grab "DECLARE @Query = ..." nodes
  public override void Visit(DeclareVariableElement node) {
    if (
      "@Query".Equals(node.VariableName.Value, StringComparison.OrdinalIgnoreCase) && 
      node.Value != null
    ) {
      QueryAssignments.Add(node.Value);
    }
  }
}

假设 procedures 是一个具有存储过程定义的 List<string>,那么我们像这样应用访问者:

foreach (string procedure in procedures) {
  TSqlFragment fragment;
  using (var reader = new StringReader(procedure)) {
    IList<ParseError> parseErrors;
    var parser = new TSql130Parser(true);  // or a lower version, I suppose
    fragment = parser.Parse(reader, out parseErrors);
    if (parseErrors.Any()) {
      // handle errors
      continue;
    }
  }
  var dynamicQueryFinder = new DynamicQueryFinder();
  fragment.Accept(dynamicQueryFinder);
  if (dynamicQueryFinder.QueryAssignments.Any()) {
    Console.WriteLine($"===== {dynamicQueryFinder.ProcedureName} =====");
    foreach (ScalarExpression assignment in dynamicQueryFinder.QueryAssignments) {
      Console.WriteLine(assignment.Script());
    }
  }
}

.Script() 是我拼凑出来的一个方便的方法,因此我们可以将片段转回纯文本:

public static class TSqlFragmentExtensions {
  public static string Script(this TSqlFragment fragment) {
    return String.Join("", fragment.ScriptTokenStream
      .Skip(fragment.FirstTokenIndex)
      .Take(fragment.LastTokenIndex - fragment.FirstTokenIndex + 1)
      .Select(t => t.Text)
    );
  }
}

这将打印分配给名为 @Query.

的变量的所有存储过程中的所有表达式

这种方法的好处在于,您可以轻而易举地解析语句,从而进行更复杂的处理,例如将字符串表达式转回其未转义的形式或寻找 EXEC(...) 的所有实例和sp_executesql(不考虑变量名),也是可以的。

缺点当然是这不是纯T-SQL。您可以使用任何您喜欢的 .NET 语言(我使用 C# 因为我最熟悉它),但它仍然涉及编写外部代码。如果您知道所有代码都遵循一种特定模式,该模式对于 T-SQL 字符串操作分析来说足够简单,则更原始的解决方案(例如 CHARINDEX 处理字符串可能会奏效。