Парсим с помощью ANTLR 4 хранимки T-SQL для получения связей между таблицами

habr.png

Доброго дня, друзья.

Задачу обозначил как абстрактную, т.е. для освоения методов решения, хотя практическое применение кто-нибудь может и найдет.

Изначально идея возникла когда я пришел в контору скуль-разработчиком. Там были сотни хранимых процедур, битая документация, не было связей между таблицами.

К делу. Далее использование Antlr и примеры кода.

Спасибо коллегам с foxclub.ru и KvanTTT за критику и это:

Да, и чота я не пойму для чего используется метод accept в шаблоне Visitor.
Я использую тока Visit*

Ответ KvanTTT

— Он вам не понадобится, т.к. используется в сгенерированном коде и вызывает необходмые методы Visit.


Antlr — эта штука генерит лексеры и парсеры на разных языках,
по-умолчанию java.

Cборка самого antlr

set PATH=c:\apache-maven-3.3.9\bin\;%PATH%  
  rem mvn -DskipTests install   
  mvn install -DskipTests=true


собрать c:\dev\antlr4\runtime\CSharp\runtime\CSharp\Antlr4.vs2013.sln

добавляем в tsql.g4(файл грамматики скуль для antlr):

  options  
  {  
  	language = CSharp;  
  }


генерим парсер с помощью gen_csharp.cmd

  java -jar c:\dev\antlr4\tool\target\antlr4-4.7-SNAPSHOT-complete.jar -o Generated_Csharp tsql.g4  
  rem java -jar c:\antlrworks2\antlrworks2\modules\ext\antlr4.jar -o Generated_Csharp tsql.g4  
  pause


Получаем восемь файлов: tsqlBaseListener.cs, tsqlBaseVisitor.cs, tsqlLexer.cs, tsqlListener.cs, tsqlParser.cs, tsqlVisitor.cs, tsql.tokens, tsqlLexer.tokens

Создать проект и добавить в него полученные файлы и ссылку
c:\dev\antlr4\runtime\CSharp\runtime\CSharp\Antlr4.Runtime\bin\net35\Release\Antlr4.Runtime.dll

Ну дальше, поправим код парсеров…, но

Antlr предоставляет два шаблона проектирования на выбор: Listener и Visitor.

Я попытался сначала разобраться с Visitor, но как оказалось он чаще используется для постоянно изменяемого кода, в котором могут быть синтаксические ошибки, и в методах используется возврат результата разбора.

А хранимые процедуры T-SQL скомпилированы, т.е. статичны и не содержат синтаксических ошибок. Да и кода с помощью Listener писать меньше. Код обхода с помощью Visitor я помещу в конце.

Итак наша программа Program.cs с помощью Listener

using System;
using System.IO;
using Antlr4.Runtime;
using Antlr4.Runtime.Misc;

namespace tsql1
{
    class Program
    {
        static void Main(string[] args)
        {
            //try
            //{

            string text = System.IO.File.ReadAllText(@"c:\dev\antlr4\grammars-v4-master\tsql\examples\dml_select.sql");
            StringReader reader = new StringReader(text);
            // В качестве входного потока символов устанавливаем ...
            AntlrInputStream input = new AntlrInputStream(reader);
            // Настраиваем лексер на этот поток
            tsqlLexer lexer = new tsqlLexer(input);
            // Создаем поток токенов на основе лексера
            CommonTokenStream tokens = new CommonTokenStream(lexer);
            // Создаем парсер
            tsqlParser parser = new tsqlParser(tokens);
            // Specify our entry point
            //tsqlParser.Query_specificationContext  
            tsqlParser.Tsql_fileContext Tsql_fileContext1 = parser.tsql_file();
            Console.WriteLine("Tsql_fileContext1.ChildCount = " + Tsql_fileContext1.ChildCount.ToString());

            // Walk it and attach our listener
            Antlr4.Runtime.Tree.ParseTreeWalker walker = new Antlr4.Runtime.Tree.ParseTreeWalker();
            AntlrTsqListener listener = new AntlrTsqListener();
            walker.Walk(listener, Tsql_fileContext1);
             //}
             //    catch (Exception e)
             //{
             //    Console.WriteLine(e.Message);
             //}
             Console.ReadKey();
        }

    }

    public class AntlrTsqListener : tsqlBaseListener
    {
        private enum JoinMode {
            Undefined,
            Where,
            Join
            };
        private JoinMode mode;
        private enum BranchType
        {
            Select,
            Table_sources,
            Search_condition
            //Join
        };
        private BranchType branch;

        private string alias = "";

        public override void EnterQuery_specification(tsqlParser.Query_specificationContext ctx)
        {
            mode = JoinMode.Undefined;
        }
        public override void EnterTable_sources(tsqlParser.Table_sourcesContext ctx)
        {
            if (ctx.ChildCount > 1)
                mode = JoinMode.Where;
            branch = BranchType.Table_sources;
        }
        public override void EnterTable_source_item_joined([NotNull] tsqlParser.Table_source_item_joinedContext ctx)
        {
            if ((mode == JoinMode.Undefined  & ctx.ChildCount == 1) | (mode == JoinMode.Where))
                return;
            mode = JoinMode.Join;
            branch = BranchType.Table_sources;
        }
        public override void EnterTable_name_with_hint([NotNull] tsqlParser.Table_name_with_hintContext ctx)
        {
            if (mode == JoinMode.Undefined)
                return;
            if (branch == BranchType.Table_sources)
                Console.WriteLine(branch.ToString());
            alias = "";
        }
        public override void EnterTable_name([NotNull] tsqlParser.Table_nameContext ctx)
        {
            if (branch == BranchType.Search_condition | branch == BranchType.Select | mode == JoinMode.Undefined)
                return;
            Console.WriteLine(ctx.GetText());
        }
        public override void EnterTable_alias([NotNull] tsqlParser.Table_aliasContext ctx)
        {
            if (branch == BranchType.Search_condition | branch == BranchType.Select | mode == JoinMode.Undefined)
                return;
            alias = ctx.GetChild(0).GetText();
            Console.WriteLine("alias=" + alias);
        }
        public override void EnterSearch_condition([NotNull] tsqlParser.Search_conditionContext ctx)
        {
            if (mode == JoinMode.Undefined)
                return;
            branch = BranchType.Search_condition;
            Console.WriteLine("Search_condition");
            Console.WriteLine(ctx.GetText());
            return;
        }
        public override void EnterSelect_statement([NotNull] tsqlParser.Select_statementContext ctx)
        {
            Console.WriteLine("Select_statement");
            branch = BranchType.Select;
            return;
        }


Входные данные:

select *
from t1, t2
where t1.id = t2.id

SELECT p.*
FROM Production.Product AS p
ORDER BY Name ASC;
GO

select *
from zxc as t1
    inner join qwe t2 on t1.id = t2.id
    inner join asd t3 on t3.id = t2.id
...


Пример вывода:

Tsql_fileContext1.ChildCount = 105
Select_statement
Table_sources
t1
Table_sources
t2
Search_condition
t1.id=t2.id
Select_statement
Select_statement
Table_sources
zxc
alias=t1
Table_sources
qwe
alias=t2
Search_condition
t1.id=t2.id
Table_sources
asd
alias=t3
Search_condition
t3.id=t2.id
...


Данный результат можно разложить в Excel если захочется.

Все.

Прошу сильно не бить. Может что-то забыл, может что-то устарело.

Грамматики для antl на github
Старая грамматика для скуль на pastebin
Antlr на antlr.org

Код обхода с помощью Visitor
  using System;  
  using System.IO;  
  using Antlr4.Runtime;  
  using Antlr4.Runtime.Misc;  
    
  namespace tsql1  
  {  
      class Program  
      {  
          static void Main(string[] args)  
          {  
              //try  
              //{  
                    
                  string text = System.IO.File.ReadAllText(@"c:\dev\antlr4\grammars-v4-master\tsql\examples\dml_insert.sql");  
                  StringReader reader = new StringReader(text);  
                  // В качестве входного потока символов устанавливаем ...  
                  AntlrInputStream input = new AntlrInputStream(reader);  
                  // Настраиваем лексер на этот поток  
                  tsqlLexer lexer = new tsqlLexer(input);  
                  // Создаем поток токенов на основе лексера  
                  CommonTokenStream tokens = new CommonTokenStream(lexer);  
                  // Создаем парсер  
                  tsqlParser parser = new tsqlParser(tokens);  
                  // Specify our entry point  
                  //tsqlParser.Query_specificationContext    
                  tsqlParser.Tsql_fileContext Tsql_fileContext1 = parser.tsql_file();  
              Console.WriteLine("Tsql_fileContext1.ChildCount = " + Tsql_fileContext1.ChildCount.ToString());  
    
              /*                // Walk it and attach our listener  
                              Antlr4.Runtime.Tree.ParseTreeWalker walker = new Antlr4.Runtime.Tree.ParseTreeWalker();  
                              AntlrTsqListener listener = new AntlrTsqListener();  
                              walker.Walk(listener, Tsql_fileContext1);*/  
              AntlrTsqVisitor visitor = new AntlrTsqVisitor();  
                  var result = visitor.Visit(Tsql_fileContext1);  
              //}  
              //    catch (Exception e)  
              //{  
              //    Console.WriteLine(e.Message);  
              //}  
              Console.ReadKey();  
          }  
    
      }  
  /*  
      public class AntlrTsqListener: tsqlBaseListener  
      {  
          public override void EnterQuery_specification(tsqlParser.Query_specificationContext ctx)  
          {  
              int ii = 0;  
              //Console.WriteLine(ctx.ToStringTree());  
              Console.WriteLine("ctx.ChildCount" + ctx.ChildCount.ToString());  
              for (ii = 0; ii < ctx.ChildCount; ++ii)  
              {  
                  Console.WriteLine("ii=" + ii.ToString());  
                  Console.WriteLine(ctx.GetChild(ii).GetType().ToString());  
                  Console.WriteLine(ctx.GetChild(ii).GetText());  
              }  
              //Console.WriteLine(ctx.GetChild().ToString());  
          }  
      }  
     */  
    
      public class AntlrTsqVisitor : tsqlBaseVisitor  
      {  
  /*  
          public override string VisitSql_clauses(tsqlParser.Sql_clausesContext ctx)  
          {  
              Console.WriteLine("VisitSql_clauses");  
              return VisitChildren(ctx).ToString();  
          }  
 */  
          public override string VisitSql_clause(tsqlParser.Sql_clauseContext ctx)  
          {  
              Console.WriteLine("VisitSql_clause");  
              try  
              {  
                  return VisitDml_clause(ctx.dml_clause()).ToString();  
              }  
              catch (Exception e)  
              {  
                  return "";  
              }  
          }  
  /*  
          public override string VisitDml_clause(tsqlParser.Dml_clauseContext ctx)  
          {  
              Console.WriteLine("VisitDml_clause");  
              return VisitChildren(ctx).ToString();  
          }  
 */  
          public override string VisitSelect_statement([NotNull] tsqlParser.Select_statementContext ctx)  
          {  
              Console.WriteLine("VisitSelect_statement");  
              return VisitTable_sources(ctx.query_expression().query_specification().table_sources()).ToString();  
          }  
          public override string VisitDelete_statement([NotNull] tsqlParser.Delete_statementContext ctx)  
          {  
              Console.WriteLine("VisitDelete_statement");  
              try  
              {  
                  return VisitTable_sources(ctx.table_sources());  
              }  
              catch (Exception e)  
              {  
                  return "";  
              }  
          }  
          public override string VisitUpdate_statement([NotNull] tsqlParser.Update_statementContext ctx)  
          {  
              Console.WriteLine("VisitUpdate_statement");  
              try  
              {  
                  return VisitTable_sources(ctx.table_sources());  
              }  
              catch (Exception e)  
              {  
                  return "";  
              }  
          }  
          public override string VisitInsert_statement([NotNull] tsqlParser.Insert_statementContext ctx)  
          {  
              Console.WriteLine("VisitInsert_statement");  
              try  
              {  
                  return VisitTable_sources(ctx.insert_statement_value().derived_table().subquery().select_statement().query_expression().query_specification().table_sources());  
              }  
              catch (Exception e)  
              {  
                  return "";  
              }  
          }  
  /*  
          public override string VisitTable_sources([NotNull] tsqlParser.Table_sourcesContext ctx)  
          {  
              Console.WriteLine("VisitTable_sources");  
              return VisitChildren(ctx).ToString();  
          }  
          public override string VisitTable_source([NotNull] tsqlParser.Table_sourceContext ctx)  
          {  
              Console.WriteLine("VisitTable_source");  
              return VisitChildren(ctx).ToString();  
          }  
          public override string VisitTable_source_item_joined([NotNull] tsqlParser.Table_source_item_joinedContext ctx)  
          {  
              Console.WriteLine("VisitTable_source_item_joined");  
              return VisitChildren(ctx).ToString();  
          }  
 */  
          public override string VisitTable_source_item([NotNull] tsqlParser.Table_source_itemContext ctx)  
          {  
              Console.WriteLine("VisitTable_source_item");  
              int ii = 0;  
              //Console.WriteLine(ctx.ToStringTree());  
              Console.WriteLine("ctx.ChildCount " + ctx.ChildCount.ToString());  
              for (ii = 0; ii < ctx.ChildCount; ++ii)  
              {  
                  Console.WriteLine("ii=" + ii.ToString());  
                  Console.WriteLine(ctx.GetChild(ii).GetType().ToString());  
                  Console.WriteLine(ctx.GetChild(ii).GetText());  
                  //if (ctx.GetChild(ii).GetType().ToString() == "tsqlParser+Table_sourcesContext")  
                  //{  
                  //    this.VisitTable_sources(ctx.table_sources());  
                  //}  
              }  
              //Console.WriteLine(ctx.GetChild().ToString());  
              return ctx.ToString();  
          }  
          public override string VisitJoin_part([NotNull] tsqlParser.Join_partContext ctx)  
          {  
              Console.WriteLine("VisitJoin_part");  
              int ii = 0;  
              //Console.WriteLine(ctx.ToStringTree());  
              Console.WriteLine("ctx.ChildCount " + ctx.ChildCount.ToString());  
              for (ii = 0; ii < ctx.ChildCount; ++ii)  
              {  
                  Console.WriteLine("ii=" + ii.ToString());  
                  Console.WriteLine(ctx.GetChild(ii).GetType().ToString());  
                  Console.WriteLine(ctx.GetChild(ii).GetText());  
                  if (ctx.GetChild(ii).GetType().ToString() == "tsqlParser+Table_sourceContext")  
                  {  
                      this.VisitTable_source(ctx.table_source());  
                  }  
              }  
              //Console.WriteLine(ctx.GetChild().ToString());  
              return ctx.ToString();  
          }  
      }  
  }

© Habrahabr.ru