1 引言
因為工作關(guān)系英妓,需要開發(fā)支持眾多方言的 SQL 編輯器也拜,所以復(fù)習(xí)了一下編譯原理相關(guān)知識泡嘴。
相比編譯原理專家丑瞧,我們只需要了解部分編譯原理即可實現(xiàn) SQL 編輯器柑土,所以這是一篇寫給前端的編譯原理文章。
解析 SQL 可以分為如下四步:
- 詞法分析绊汹,將 SQL 字符串拆分成包含關(guān)鍵詞識別的字符段(Tokens)稽屏。
- 語法分析,利用自頂向下或自底向上的算法西乖,將 Tokens 解析為 AST狐榔,可以手動,也可以自動获雕。
- 錯誤檢測薄腻、恢復(fù)、提示推斷届案,都需要利用語法分析產(chǎn)生的 AST庵楷。
- 語義分析,做完這一步就可以執(zhí)行 SQL 語句了萝玷,不過對前端而言嫁乘,不需要深入到這一步昆婿,可以跳過球碉。
2 精讀
詞法分析就像刀削面的過程,拿著一段字符串(面條)一端不斷下刀仓蛆,當(dāng)面條被切完也就完成了詞法分析睁冬,所以詞法分析是 字符串 -> 一堆字符段 的過程。
流程很簡單看疙,難點就在下刀的分寸了豆拨,每次砍幾厘米呢?
回到詞法分析能庆,為了準(zhǔn)備切分施禾,我們需要定義 SQL 的 Token 有哪些類型,即 Token 分類搁胆。
Token 分類
SQL 的 Token 可以分為如下幾類:
- 注釋弥搞。
- 關(guān)鍵字(
SELECT
邮绿、CREATE
)。 - 操作符(
+
攀例、-
船逮、>=
)。 - 開閉合標(biāo)志(
(
粤铭、CASE
)挖胃。 - 占位符(
?
)。 - 空格梆惯。
- 引號包裹的文本酱鸭、數(shù)字、字段垛吗。
- 方言語法(
${variable}
)凛辣。
可以看到,在詞法分析階段职烧,我們的 Tokens 不需要關(guān)心關(guān)鍵詞是什么扁誓,只要識別是不是關(guān)鍵詞即可,因為關(guān)鍵詞的辨認(rèn)會留到語法分析時處理蚀之。涉及到語意處理就要考慮上下文蝗敢,而這都不是詞法分析階段要考慮的。
同樣足删,操作符寿谴、空格、文本失受、占位符等構(gòu)成了 SQL 語句的其他部分讶泰,最后通過開閉合標(biāo)志比如左括號和右括號,讓 SQL 支持子語句拂到。
再強調(diào)一次痪署,雖然 SQL 支持子語句,但并不是放在任何位置都是合理的兄旬,其他類型 Token 同理狼犯,但是詞法分析不需要考慮 Token 是否合理,只要切分即可领铐。
用正則逐段分詞
像大多數(shù)語言一樣悯森,SQL 為了方便人類閱讀,采用從左到右的書寫方式绪撵,因此分詞方向也從左到右瓢姻。
我們?yōu)槊總€ Token 類型寫一個函數(shù),比如匹配空格的匹配函數(shù):
function getTokenWhitespace(restStr: string) {
const matches = restStr.match(/^(\s+)/);
if (matches) {
return { type, value: matches[1] };
}
}
restStr
表示掐去頭部剩下的 SQL 字符串音诈,所有匹配函數(shù)都拿 restStr
進(jìn)行匹配幻碱,已經(jīng)匹配的不需要再處理续膳。
通過正則 /^(\s+)/
匹配到第一個以空格開頭的空格(讀起來有點別扭),匹配時必須保證以你要匹配的內(nèi)容開頭收班,而且只匹配一次坟岔,這樣才不會在切詞時發(fā)生遺漏。
同理匹配 /**/
類型注釋時摔桦,也能通過正則輕而易舉的實現(xiàn):
function getTokenBlockComment(restStr: string) {
const matches = restStr.match(/^(\/\*[^]*?(?:\*\/|$))/);
if (matches) {
return { type, value: matches[1] };
}
}
其中 (?:\*/\)
表示匹配到以 */
結(jié)尾處社付,而 (?:\*\/|$)
后面的 |$
表示或者直接匹配到結(jié)尾(如果一直沒有遇到 */
那后面全部當(dāng)作注釋)。
所以只要 Token 分類得當(dāng)邻耕,并且能為每一個分類寫一個頭匹配正則鸥咖,分詞功能就實現(xiàn)了 90%。
方言拓展
為了支持某些方言兄世,需要從分詞時就開始做考慮啼辣。比如 ${variable}
作為一種變量用法時,我們需要在普通字段的正則匹配中御滩,加入一項 \$\{[a-zA-Z0-9]+\}
匹配鸥拧。
如果要支持純中文作為字段,可以再補充 |\u4e00-\u9fa5
削解。
分詞主流程
有了一個個分詞函數(shù)富弦,再補充一個不斷匹配、切割字符串氛驮、再匹配的主函數(shù)即可腕柜,這一步更簡單:
while (sqlStr) {
token =
getTokenWhitespace(sqlStr, token) | getTokenBlockComment(sqlStr, token);
sqlStr = sqlStr.substring(token.value.length);
tokens.push(token);
}
上面的函數(shù)每取一次 Token,都將取到的 Token 長度丟掉矫废,繼續(xù)匹配剩下的字符串盏缤,直到字符串被切分完為止。
有些特殊情況需要拿到上次的 Token 才能判斷下一個 Token 該如何切割蓖扑,所以將 Token 傳給每一個下一步 Match 函數(shù)唉铜。
最后,執(zhí)行這個主函數(shù)赵誓,分詞就完成了打毛!
3 總結(jié)
分詞比較簡單,到這里就全部結(jié)束了俩功。后面即將進(jìn)入深水區(qū)語法分析,敬請期待碰声。
4 更多討論
如果你想?yún)⑴c討論诡蜓,請點擊這里,每周都有新的主題胰挑,周末或周一發(fā)布蔓罚。