T-SQL の パーサーとジェネレーター(ScriptDom)の紹介

SQL Server 用の SQL である T-SQL には、パーサーとジェネレーターが MS から .NET Framework のライブラリとして提供されています。


最新の 2012 ではこちら
Microsoft.SqlServer.TransactSql.ScriptDom 名前空間 ()
一つ前はこちら
Microsoft.Data.Schema.ScriptDom 名前空間 ()
Microsoft.Data.Schema.ScriptDom.Sql 名前空間 ()
名前空間が変わっていますね!
昔の方は、インターフェースと実装を分けていて他のDBでも使えるようになっていましたが、最新では T-SQL(SQL Server)一本になっています。*1

どんなところで使うのかというと、VS2010 のころの記事ですが SQL の静的解析で使用するチュートリアルがあります。
チュートリアル: SQL 用のカスタムの静的コード分析規則アセンブリを作成する


最新版のインストールは、マイクロソフト公式ダウンロード センターから Microsoft® SQL Server® 2012 Feature Pack をダウンロード の「Microsoft〓 SQL Server〓 2012 Transact-SQL ScriptDom」をインストールします。
SQL Server 2012 をインストールしている環境では、もしかしたら既にインストール済みと出るかもしれません。


使い方は、アセンブリの参照で、Microsoft.SqlServer.TransactSql.ScriptDom を追加します。

では、パースしてからクエリを整形して再生成してみましょう。

using Microsoft.SqlServer.TransactSql.ScriptDom;
using System;
using System.Collections.Generic;
using System.IO;

class Program {
  static void Main(string[] args) {
    ParseAndFormat();
    Console.ReadKey();
  }
  static void ParseAndFormat() {
    var query = @"select [Id], [Name], [EntryDate] 
from [Table_1] 
where [EntryDate] >= @EntryDateFrom and [Name] like @Name 
order by [Id] desc";
    var parser = new TSql110Parser(false);
    IList<ParseError> errors;
    var parsed = parser.Parse(new StringReader(query), out errors);
    if (errors.Count != 0) { throw new Exception("パース失敗してるよ!"); }
    
    var options = new SqlScriptGeneratorOptions() {
      KeywordCasing = KeywordCasing.Uppercase, 
      IncludeSemicolons = true,
      NewLineBeforeFromClause = true, 
      NewLineBeforeOrderByClause = true, 
      NewLineBeforeWhereClause = true
    };
    var generator = new Sql110ScriptGenerator(options);
    string formated;
    generator.GenerateScript(parsed, out formated);
    Console.WriteLine(formated);
  }
}


ジェネレータに渡すオプションによって、生成されるクエリの書式を変更出来ます。詳細はこちらからどうぞ。
SqlScriptGeneratorOptions クラス (Microsoft.SqlServer.TransactSql.ScriptDom)


パース時にエラーになった場合の例はこちら。

using Microsoft.SqlServer.TransactSql.ScriptDom;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

class Program {
  static void Main(string[] args) {
    ParseError();
    Console.ReadKey();
  }
  static void ParseError() {
    var query = @"select [Id], getdate() as [sysdate], 
from [Table_1] 
order by [Id] desc";
    var parser = new TSql110Parser(false);
    IList<ParseError> errors;
    var parsed = parser.Parse(new StringReader(query), out errors);

    Console.WriteLine(string.Join(
      Environment.NewLine, 
      errors.Select(e => string.Format("行:{0} {1}文字目 {2}", e.Line, e.Column, e.Message))
    ));
  }
}


エラーは複数返ってくる API になっていますが、SSMS(SQL Server Management Studio) 等で実行した時のように、1バッチ1エラーになります。*2複数バッチのクエリを渡して、それぞれでエラーがあれば複数件のエラーが返ってきます。

using Microsoft.SqlServer.TransactSql.ScriptDom;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

class Program {
  static void Main(string[] args) {
    ParseErrorMultiBatch();
    Console.ReadKey();
  }
  static void ParseErrorMultiBatch() {
    var query = @"select [Id], getdate() as [sysdate], 
from [Table_1] 
where [Id] desc
go
insert int [Table_1] values (1, 'aaa', getdate())";
    var parser = new TSql110Parser(false);
    IList<ParseError> errors;
    var parsed = parser.Parse(new StringReader(query), out errors);

    Console.WriteLine(string.Join(
      Environment.NewLine,
      errors.Select(e => string.Format("行:{0} {1}文字目 {2}", e.Line, e.Column, e.Message))
    ));
  }
}


最初の select には、2箇所文法エラーがあります(from の前のカンマと order by が where になっている)が、エラーとして引っかかるのは最初の物だけです。
GO (Transact-SQL) を使って、複数バッチのクエリにしているのでエラーもそれぞれのバッチ毎に表示されます。


パースした戻り値の型は、TSqlFragment クラス (Microsoft.SqlServer.TransactSql.ScriptDom) になります。
MSDN で継承階層を見てもらうとわかりますがえらい事になっています。また、ここで表示されている継承したクラスのそれぞれの継承階層も大変な事になっています。
というわけで、これを全部解説していると膨大になる*3ので、とりあえずパースせずにクエリを生成するサンプルを載せておきます。

using Microsoft.SqlServer.TransactSql.ScriptDom;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

class Program {
  static void Main(string[] args) {
    GenerateQuery();
    Console.ReadKey();
  }
  static void GenerateQuery() {
    var q = new QuerySpecification();
    var scol1 = new MultiPartIdentifier();
    scol1.Identifiers.Add(new Identifier() { Value = "id" });
    q.SelectElements.Add(new SelectScalarExpression() {
      Expression = new ColumnReferenceExpression() {
        MultiPartIdentifier = scol1
      }
    });
    var scol2 = new MultiPartIdentifier();
    scol2.Identifiers.Add(new Identifier() { Value = "name" });
    q.SelectElements.Add(new SelectScalarExpression() {
      Expression = new ColumnReferenceExpression() {
        MultiPartIdentifier = scol2
      }
    });
    q.FromClause = new FromClause();
    var sc = new SchemaObjectName();
    sc.Identifiers.Add(new Identifier() { Value = "Table_01" });
    q.FromClause.TableReferences.Add(new NamedTableReference() {
      SchemaObject = sc
    });
    var col = new MultiPartIdentifier();
    col.Identifiers.Add(new Identifier() { Value = "id" });
    q.WhereClause = new WhereClause() {
      SearchCondition = new BooleanComparisonExpression() {
        FirstExpression = new ColumnReferenceExpression() {
          ColumnType = ColumnType.Regular,
          MultiPartIdentifier = col
        },
        SecondExpression = new IntegerLiteral() {
          Value = "10"
        },
        ComparisonType = BooleanComparisonType.Equals
      }
    };

    var generator = new Sql110ScriptGenerator();
    string result;
    generator.GenerateScript(new SelectStatement() { QueryExpression = q }, out result);
    Console.WriteLine(result);
  }
}


この程度のクエリを生成するのに、このコード量!さらにテーブル名やカラム名は文字列指定でコンパイル時にチェック出来ないと割に合いませんね。
こいつを使ってクエリを生成しようと思ったら、もう1個か2個位ラッパーが要りそうです。


ASP.NET MVC の勉強がてらにお試しサイト用意してみました。パースして整形したクエリを下に表示するだけ。
ScriptDom Sample
初期

エラー時

エラー無で整形

あと幾つかネタはあるので、また今度紹介します。

*1:誰も実装しなかったのかな。。

*2:1バッチ複文でも、最初に見つかったエラーのみ

*3:全部見きれてないだけ