由於工做關係,須要開發支持衆多方言的 SQL 編輯器,因此複習了一下編譯原理相關知識。前端
相比編譯原理專家,咱們只須要了解部分編譯原理便可實現 SQL 編輯器,因此這是一篇寫給前端的編譯原理文章。git
解析 SQL 能夠分爲以下四步:github
詞法分析就像刀削麪的過程,拿着一段字符串(麪條)一端不斷下刀,當面條被切完也就完成了詞法分析,因此詞法分析是 字符串 -> 一堆字符段 的過程。算法
流程很簡單,難點就在下刀的分寸了,每次砍幾釐米呢?sql
回到詞法分析,爲了準備切分,咱們須要定義 SQL 的 Token 有哪些類型,即 Token 分類。typescript
SQL 的 Token 能夠分爲以下幾類:編輯器
SELECT
、CREATE
)。+
、-
、>=
)。(
、CASE
)。?
)。${variable}
)。能夠看到,在詞法分析階段,咱們的 Tokens 不須要關心關鍵詞是什麼,只要識別是否是關鍵詞便可,由於關鍵詞的辨認會留到語法分析時處理。涉及到語意處理就要考慮上下文,而這都不是詞法分析階段要考慮的。函數
一樣,操做符、空格、文本、佔位符等構成了 SQL 語句的其餘部分,最後經過開閉合標誌好比左括號和右括號,讓 SQL 支持子語句。spa
再強調一次,雖然 SQL 支持子語句,但並非放在任何位置都是合理的,其餘類型 Token 同理,可是詞法分析不須要考慮 Token 是否合理,只要切分便可。rest
像大多數語言同樣,SQL 爲了方便人類閱讀,採用從左到右的書寫方式,所以分詞方向也從左到右。
咱們爲每一個 Token 類型寫一個函數,好比匹配空格的匹配函數:
function getTokenWhitespace(restStr: string) { const matches = restStr.match(/^(\s+)/); if (matches) { return { type, value: matches[1] }; } }
restStr
表示掐去頭部剩下的 SQL 字符串,全部匹配函數都拿 restStr
進行匹配,已經匹配的不須要再處理。
經過正則 /^(\s+)/
匹配到第一個以空格開頭的空格(讀起來有點彆扭),匹配時必須保證以你要匹配的內容開頭,並且只匹配一次,這樣纔不會在切詞時發生遺漏。
同理匹配 /**/
類型註釋時,也能經過正則垂手可得的實現:
function getTokenBlockComment(restStr: string) { const matches = restStr.match(/^(\/\*[^]*?(?:\*\/|$))/); if (matches) { return { type, value: matches[1] }; } }
其中 (?:\*/\)
表示匹配到以 */
結尾處,而 (?:\*\/|$)
後面的 |$
表示或者直接匹配到結尾(若是一直沒有遇到 */
那後面所有看成註釋)。
因此只要 Token 分類得當,而且能爲每個分類寫一個頭匹配正則,分詞功能就實現了 90%。
爲了支持某些方言,須要從分詞時就開始作考慮。好比 ${variable}
做爲一種變量用法時,咱們須要在普通字段的正則匹配中,加入一項 \$\{[a-zA-Z0-9]+\}
匹配。
若是要支持純中文做爲字段,能夠再補充 |\u4e00-\u9fa5
。
有了一個個分詞函數,再補充一個不斷匹配、切割字符串、再匹配的主函數便可,這一步更簡單:
while (sqlStr) { token = getTokenWhitespace(sqlStr, token) | getTokenBlockComment(sqlStr, token); sqlStr = sqlStr.substring(token.value.length); tokens.push(token); }
上面的函數每取一次 Token,都將取到的 Token 長度丟掉,繼續匹配剩下的字符串,直到字符串被切分完爲止。
有些特殊狀況須要拿到上次的 Token 才能判斷下一個 Token 該如何切割,因此將 Token 傳給每個下一步 Match 函數。
最後,執行這個主函數,分詞就完成了!
分詞比較簡單,到這裏就所有結束了。後面即將進入深水區語法分析,敬請期待。
討論地址是: 精讀《手寫 SQL 編譯器 - 詞法分析》 · Issue #93 · dt-fe/weekly
若是你想參與討論,請點擊這裏,每週都有新的主題,週末或週一發佈。