Парсим с помощью ANTLR 4 хранимки T-SQL для получения связей между таблицами
Доброго дня, друзья.
Задачу обозначил как абстрактную, т.е. для освоения методов решения, хотя практическое применение кто-нибудь может и найдет.
Изначально идея возникла когда я пришел в контору скуль-разработчиком. Там были сотни хранимых процедур, битая документация, не было связей между таблицами.
К делу. Далее использование 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
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();
}
}
}