如何使用scriptdom API提取跨数据库引用

Jus*_*ner 3 t-sql sql-server parsing foreign-keys scriptdom

Microsoft已公开scriptdomAPI以解析和生成TSQL.我是新手,还在玩它.我想知道如何从像这样的查询中获取跨数据库引用.

UPDATE  t3
SET     description = 'abc'
FROM    database1.dbo.table1 t1
        INNER JOIN database2.dbo.table2 t2
            ON (t1.id = t2.t1_id)
        LEFT OUTER JOIN database3.dbo.table3 t3
            ON (t3.id = t2.t3_id)
        INNER JOIN database2.dbo.table4 t4
            ON (t4.id = t2.t4_id)
Run Code Online (Sandbox Code Playgroud)

我想要的是一个参考列表:

database1.dbo.table1.id = database2.dbo.table2.t1_id
database3.dbo.table3.id = database2.dbo.table2.t3_id
database2.dbo.table4.id = database2.dbo.table2.t4_id
Run Code Online (Sandbox Code Playgroud)

但是,对于最后一个条目database2.dbo.table4.id = database2.dbo.table2.t4_id,来自2端的两个列都来自同一个数据库database2,这不是我想要的.所以我最后要求的结果是:

database1.dbo.table1.id = database2.dbo.table2.t1_id
database3.dbo.table3.id = database2.dbo.table2.t3_id
Run Code Online (Sandbox Code Playgroud)

有可能实现scriptdom吗?

Jer*_*ert 9

强大的实施并不容易.对于这个问题中提出的有限问题,解决方案相对简单 - 压力"相对".我假设如下:

  • 查询只有一个级别 - 没有UNION,子查询,WITH表达式或其他引入别名新范围的东西(这可能会很快变得复杂).
  • 查询中的所有标识符都是完全限定的,因此毫无疑问它指的是什么对象.

解决方案策略如下所示:我们首先访问TSqlFragment以创建所有表别名的列表,然后再次访问它以获取所有equijoins,沿途扩展别名.使用该列表,我们确定不引用同一数据库的等值连接列表.在代码中:

var sql = @"
  UPDATE  t3
  SET     description = 'abc'
  FROM    database1.dbo.table1 t1
      INNER JOIN database2.dbo.table2 t2
        ON (t1.id = t2.t1_id)
      LEFT OUTER JOIN database3.dbo.table3 t3
        ON (t3.id = t2.t3_id)
      INNER JOIN database2.dbo.table4 t4
        ON (t4.id = t2.t4_id)

";                

var parser = new TSql120Parser(initialQuotedIdentifiers: false);
IList<ParseError> errors;
TSqlScript script;
using (var reader = new StringReader(sql)) {
  script = (TSqlScript) parser.Parse(reader, out errors);
}
// First resolve aliases.
var aliasResolutionVisitor = new AliasResolutionVisitor();
script.Accept(aliasResolutionVisitor);

// Then find all equijoins, expanding aliases along the way.
var findEqualityJoinVisitor = new FindEqualityJoinVisitor(
  aliasResolutionVisitor.Aliases
);
script.Accept(findEqualityJoinVisitor);

// Now list all aliases where the left database is not the same
// as the right database.
foreach (
  var equiJoin in 
  findEqualityJoinVisitor.EqualityJoins.Where(
    j => !j.JoinsSameDatabase()
  )
) {
  Console.WriteLine(equiJoin.ToString());
}
Run Code Online (Sandbox Code Playgroud)

输出:

database3.dbo.table3.id = database2.dbo.table2.t3_id
database1.dbo.table1.id = database2.dbo.table2.t1_id
Run Code Online (Sandbox Code Playgroud)

AliasResolutionVisitor 是一件简单的事情:

public class AliasResolutionVisitor : TSqlFragmentVisitor {
  readonly Dictionary<string, string> aliases = new Dictionary<string, string>();
  public Dictionary<string, string> Aliases { get { return aliases; } }

  public override void Visit(NamedTableReference namedTableReference ) {
    Identifier alias = namedTableReference.Alias;
    string baseObjectName = namedTableReference.SchemaObject.AsObjectName();
    if (alias != null) {
      aliases.Add(alias.Value, baseObjectName);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

我们只需查看查询中的所有命名表引用,如果它们有别名,则将其添加到字典中.请注意,如果引入子查询,这将失败,因为此访问者没有范围概念(实际上,向访问者添加范围要困难得多,因为TSqlFragment提供无法注释解析树甚至从节点中移动它).

EqualityJoinVisitor更有趣的是:

public class FindEqualityJoinVisitor : TSqlFragmentVisitor {
  readonly Dictionary<string, string> aliases;
  public FindEqualityJoinVisitor(Dictionary<string, string> aliases) {
    this.aliases = aliases;
  }

  readonly List<EqualityJoin> equalityJoins = new List<EqualityJoin>();
  public List<EqualityJoin> EqualityJoins { get { return equalityJoins; } }

  public override void Visit(QualifiedJoin qualifiedJoin) {
    var findEqualityComparisonVisitor = new FindEqualityComparisonVisitor();
    qualifiedJoin.SearchCondition.Accept(findEqualityComparisonVisitor);
    foreach (
      var equalityComparison in findEqualityComparisonVisitor.Comparisons
    ) {
      var firstColumnReferenceExpression = 
        equalityComparison.FirstExpression as ColumnReferenceExpression
      ;
      var secondColumnReferenceExpression = 
        equalityComparison.SecondExpression as ColumnReferenceExpression
      ;
      if (
        firstColumnReferenceExpression != null && 
        secondColumnReferenceExpression != null
      ) {
        string firstColumnResolved = resolveMultipartIdentifier(
          firstColumnReferenceExpression.MultiPartIdentifier
        );
        string secondColumnResolved = resolveMultipartIdentifier(
          secondColumnReferenceExpression.MultiPartIdentifier
        );
        equalityJoins.Add(
          new EqualityJoin(firstColumnResolved, secondColumnResolved)
        );
      }
    }
  }

  private string resolveMultipartIdentifier(MultiPartIdentifier identifier) {
    if (
      identifier.Identifiers.Count == 2 && 
      aliases.ContainsKey(identifier.Identifiers[0].Value)
    ) {
      return 
        aliases[identifier.Identifiers[0].Value] + "." + 
        identifier.Identifiers[1].Value;
    } else {
      return identifier.AsObjectName();
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

这会搜索QualifiedJoin实例,如果我们找到它们,我们会检查搜索条件以查找所有出现的相等比较.请注意,这适用于嵌套搜索条件:在Bar JOIN Foo ON Bar.Quux = Foo.Quux AND Bar.Baz = Foo.Baz,我们将找到两个表达式.

我们如何找到它们?使用另一个小访客:

public class FindEqualityComparisonVisitor : TSqlFragmentVisitor {
  List<BooleanComparisonExpression> comparisons = 
    new List<BooleanComparisonExpression>()
  ;
  public List<BooleanComparisonExpression> Comparisons { 
    get { return comparisons; } 
  }

  public override void Visit(BooleanComparisonExpression e) {
    if (e.IsEqualityComparison()) comparisons.Add(e);
  }
}
Run Code Online (Sandbox Code Playgroud)

这里没什么复杂的.将此代码折叠到其他访问者中并不困难,但我认为这更清楚.

就是这样,除了一些帮助代码,我将在没有评论的情况下提出:

public class EqualityJoin {
  readonly SchemaObjectName left;
  public SchemaObjectName Left { get { return left; } }

  readonly SchemaObjectName right;
  public SchemaObjectName Right { get { return right; } }

  public EqualityJoin(
    string qualifiedObjectNameLeft, string qualifiedObjectNameRight
  ) {
    var parser = new TSql120Parser(initialQuotedIdentifiers: false);
    IList<ParseError> errors;
    using (var reader = new StringReader(qualifiedObjectNameLeft)) {
      left = parser.ParseSchemaObjectName(reader, out errors);
    }
    using (var reader = new StringReader(qualifiedObjectNameRight)) {
      right = parser.ParseSchemaObjectName(reader, out errors);
    }
  }

  public bool JoinsSameDatabase() {
    return left.Identifiers[0].Value == right.Identifiers[0].Value;
  }

  public override string ToString() {
    return String.Format("{0} = {1}", left.AsObjectName(), right.AsObjectName());
  }
}

public static class MultiPartIdentifierExtensions {
  public static string AsObjectName(this MultiPartIdentifier multiPartIdentifier) {
    return string.Join(".", multiPartIdentifier.Identifiers.Select(i => i.Value));
  }
}

public static class ExpressionExtensions {
  public static bool IsEqualityComparison(this BooleanExpression expression) {
    return 
      expression is BooleanComparisonExpression && 
      ((BooleanComparisonExpression) expression).ComparisonType == BooleanComparisonType.Equals
    ;
  }
}
Run Code Online (Sandbox Code Playgroud)

正如我之前提到的,这段代码非常脆弱.它假设查询具有特定的形式,如果它们不这样做,它可能会失败(非常糟糕,通过给出误导性的结果).一个主要的开放挑战是扩展它,以便它可以正确处理范围和不合格的引用,以及T-SQL脚本可以具有的其他奇怪之处,但我认为它仍然是一个有用的起点.