当前位置: 首页 > 科技观察

在Go中构建SQL解析器

时间:2023-03-19 20:28:48 科技观察

摘要本文旨在简要介绍如何在Go中构建LL(1)解析器,在本例中用于解析SQL查询。为简单起见,我们将处理子选择、函数、复杂的嵌套表达式以及所有SQL风格都支持的其他特性。这些属性与我们将使用的策略密切相关。1分钟理论解析器由两部分组成:词法分析:又名“Tokeniser”语法分析:AST创建词法分析让我们用一个例子来定义它。“标记化”以下查询:SELECTid,nameFROM'users.csv'表示提取构成此查询的“标记”。tokeniser的结果是这样的:[]string{"SELECT","id",",","name","FROM","'users.csv'"}语法分析这部分实际上是我们看的地方在tokens处,确保它们有意义并解析它们以构建某种结构,该结构以一种对将要使用它的应用程序更方便的方式表示查询(例如,用于执行查询,用颜色突出显示)。经过这一步,我们会得到这样的结果:query{Type:"Select",TableName:"users.csv",Fields:["id","name"],}解析失败的原因有很多,So同时执行这两个步骤可能会很方便,如果发生错误则立即停止。策略我们将这样定义一个解析器:typeparserstruct{sqlstring//Thequerytoparseiint//Wherewearnthequeryqueryquery.Query//我们将构建的“querystruct”stepstep//这是什么?继续阅读...}//Mainfunctionthatreturnsthe"querystruct"oranerrorfunc(p*parser)Parse()(query.Query,error){}//返回sthenexttokentoparsefunc(p*parser)peek()(string){}//sameaspeek(),butadvancingour"i"indexfunc(p*parser)pop()(string){}直观上,我们做的第一件事是“peek()***tokens”。在基本的SQL语法中,只有几个有效的初始标记:SELECT、UPDATE、DELETE等;其他一切都是错误的。像这样的代码:switchstrings.ToUpper(parser.peek()){case"SELECT":parser.query.type="SELECT"//startbuildingthe"querystruct"parser.pop()//TODOcontinuewithSELECTqueryparsing...case"UPDATE"://TODOhandleUPDATE//TODOothercases...default:returnparser.query,fmt.Errorf("invalidquerytype")}我们基本上可以填入TODO让它跑起来!然而,精明的读者会注意到解析整个SELECT查询的代码很快就会变得混乱,而且我们有许多类型的查询需要解析。所以我们需要一些结构。有限状态机(FSM)是一个非常有趣的话题,但我们不在这里讨论它,所以我们不会深入探讨。让我们专注于我们需要的东西。在我们的解析过程中,在任何给定的点(与其说是“点”,不如说是“节点”),只有少数几个token是有效的,找到这些token后,我们将进入新的节点,不同的token是有效的,依此类推,直到完成查询的解析。我们可以将这些节点关系可视化为一个有向图:点转换可以用一个更简单的表来定义,但是:我们可以直接将这个表转换成一个非常大的switch语句。我们将使用我们之前定义的parser.step属性:func(p*parser)Parse()(query.Query,error){parser.step=stepType//initialstepforparser.i=","<=","!=",","=",">","<","SELECT","INSERTINTO","VALUES","UPDATE","DELETEFROM","WHERE","FROM","SET",}此外,我们可能会遇到带引号的字符串或纯ID字符(例如字段名称)。这是一个完整的peekWithLength()实现:func(p*parser)peekWithLength()(string,int){ifp.i>=len(p.sql){return"",0}for_,rWord:=rangereservedWords{token:=p.sql[p.i:min(len(p.sql),p.i+len(rWord))]upToken:=strings.ToUpper(token)ifupToken==rWord{returnupToken,len(upToken)}}ifp.sql[p.i]=='\''{//Quotedstringreturnp.peekQuotedStringWithLength()}returnp.peekIdentifierWithLength()}其余函数比较简单,留给读者练习。如果您有兴趣,可以查看指向github的链接,其中包含完整的源代码实现。最终验证解析器可能会在获取完整的查询定义之前找到字符串的结尾。实现一个parser.validate()函数可能是个好主意,该函数查看生成的“查询”结构并在不完整或错误时返回错误。测试Go的表驱动测试模式非常适合我们的情况:=[]testCase{{Name:"emptyqueryfails",SQL:"",Expected:query.Query{},Err:fmt.Errorf("querytypecannotbeempty"),},{Name:"SELECTwithoutFROMfails",SQL:"SELECT",Expected:query.Query{Type:query.Select},Err:fmt.Errorf("tablenamecannotbeempty"),},...像这样测试测试用例:for_,tc:=rangets{t.Run(tc.Name,func(t*testing.T){actual,err:=Parse(tc.SQL)iftc.Err!=nil&&err==nil{t.Errorf("Errorshouldhavebeen%v",tc.Err)}iftc.Err==nil&&err!=nil{t.Errorf("Errorshouldhavebeennilbutwas%v",err)}iftc.Err!=nil&&err!=nil{require.Equal(t,tc.Err,err,"意外错误")}iflen(actual)>0{require.Equal(t,tc.Expected,actual[0],"Querydidn'tmatchexpectation")}})}我使用verify是因为它在查询结构不匹配时提供差异输出深入理解这个实验非常适合:学习LL(1)解析器算法自定义解析没有依赖关系的简单语法但是这种方法可能会变得乏味并且有局限性。考虑如何解析任意复杂的复合表达式(例如sqrt(a)=(1*(2+3)))。对于更强大的解析模型,请查看解析器组合器。goyacc是一个流行的Go实现。以下是完整解析器地址(或点击阅读原文):http://github.com/marianogappa/sqlparser