javacc 會(huì)根據(jù) parser.jj
中定義的相互穿插的 Token论矾、Java 代碼來(lái)自動(dòng)生成 org.apache.calcite.sql.parser.impl.SqlParserImpl
的代碼杆勇。本文期望以一個(gè)簡(jiǎn)單的 Select 語(yǔ)句為例來(lái)說(shuō)清楚 Sql 語(yǔ)句、Sql 語(yǔ)法定義蚜退、SqlParser 之間的關(guān)系。
sql 文本如下:
select * from emp where empno > 5 and gender = 'F'
Parser.jj
(語(yǔ)法定義文件)和類(lèi) SqlParserImpl
中的 SqlSelect
部分定義如下(左為 Parser.jj
钻注、右為 SqlParserImpl
類(lèi)),SqlParserImpl
是由 JavaCC 根據(jù) Parser.jj
定義的語(yǔ)法自動(dòng)生成幅恋,自動(dòng)生成說(shuō)白了也就是根據(jù)什么樣的語(yǔ)法定義生成什么樣的 java 代碼,我們希望搞明白的就是這樣的映射關(guān)系:
一淑翼、方法聲明
會(huì)根據(jù)
SqlSelect SqlSelect()
生成
final public SqlSelect SqlSelect() throws ParseException
規(guī)則也很簡(jiǎn)單:頭加 final public
,尾加 throws ParseException
二窒舟、Java 代碼調(diào)用
在 Parser.jj 中,JavaCC 對(duì)于 Java 代碼調(diào)用是直接將其復(fù)制到 Parser 的相應(yīng)位置
2.1惠豺、聲明/初始化
在 Parser.jj
中,使用 {}
包圍的部分都是代碼聲明风宁,這部分代碼會(huì)被直接 Copy 為 Parser 相應(yīng)的代碼,如:
這部分代碼的作用是聲明用于聲明一些局部變量热监,這些局部變量會(huì)通過(guò)后續(xù)的 Token 解析和代碼調(diào)用來(lái)賦值,最終用于構(gòu)造 SqlSelect
2.2孝扛、代碼調(diào)用
如下箭頭所指即語(yǔ)法定義中的代碼調(diào)用被直接復(fù)制到 Parser 的相應(yīng)位置
三幽崩、Token 校驗(yàn)
在 Parser.jj 中定義了 token < SELECT: "SELECT" >
,在 Parser.jj
中定義的語(yǔ)法要去匹配這個(gè) Token慌申,則在相應(yīng)的位置寫(xiě)一個(gè) <SELECT>
即可,JavaCC 會(huì)在 Parser 的相應(yīng)位置增加一行 jj_consume_token(SELECT)
方法。
我們知道您炉,詞法解析器會(huì)將一段 Sql 解析為一個(gè) Token list(有序的),當(dāng)我們拿一組 Token 去匹配一段語(yǔ)法定義時(shí)赚爵,每次遇到語(yǔ)法中如上所述的 Token 定義(我們這里稱(chēng)之為 expectedToken(s)
),就會(huì)從 Token list 中取出一個(gè)或多個(gè)連續(xù)的 Token(我們稱(chēng)之為 actualToken(s)
)冀膝,會(huì)去校驗(yàn)實(shí)際的和期望的 kind 是否一致:
- 如果兩者類(lèi)型一致,繼續(xù)往下走代碼生成
- 如果兩者類(lèi)型不一致畸写,說(shuō)明 Sql 文本(可能是局部的)與當(dāng)前的定義不匹配驮瞧,拋異常
jj_consume_token
的主要實(shí)現(xiàn)邏輯如下(去除了一些非關(guān)鍵代碼):
// SELECT 的 kind 值為 489
final private Token jj_consume_token(int kind) throws ParseException {
Token oldToken;
if ((oldToken = token).next != null) {
// 只要不是最后一個(gè) token,取 next Token 為當(dāng)前 token论笔;即將 Token 往后移一位。
// token 初始值為 null狂魔,第一次往后移一位后得到的是 sql 即系出來(lái)的 Token list 的第一個(gè)
// 在本例中為 SELECT
token = token.next;
} else {
token = token.next = token_source.getNextToken();
}
if (token.kind == kind) {
// 真實(shí)的 Token 類(lèi)型與期望的 Token 類(lèi)型相同,則校驗(yàn)通過(guò)
return token;
} else {
// 真實(shí)的 Token 類(lèi)型與期望的 Token 類(lèi)型不同整份,則校驗(yàn)不通過(guò),并拋異常
token = oldToken;
jj_kind = kind;
throw generateParseException();
}
}
舉幾個(gè)例子烈评,比如:<SELECT>
會(huì)生成 jj_consume_token(SELECT)
代碼,在 SqlSelect()
的語(yǔ)法定義中讲冠,是定義的第一個(gè) Token,所以這里檢查的是第一個(gè) Token 是不是 SELECT竿开;而且這里是單個(gè)、必選的否彩,不是可能是多個(gè)或者可選的
關(guān)于 Token 校驗(yàn)更加復(fù)雜的情況,我們將在后文中介紹
四嗦随、正則相關(guān)
4.1胳搞、可選
// 使用正則的 [] 表示這一部分是可選的,這部分包含了 token <HINT_BEG> 和 <COMMENT_END> 以及兩者之間的方法調(diào)用
// 如 /*+ NO_HASH_JOIN, RESOURCE(mem='128mb', parallelism='24') */
// - **/*+** 即 <HINT_BEG>
// - ***/** 即 <COMMENT_END>
// - **NO_HASH_JOIN, RESOURCE(mem='128mb', parallelism='24')** 會(huì)喂給 CommaSepatatedSqlHints(hints) 的語(yǔ)法筷转、代碼用來(lái)為 hints 賦值
[
<HINT_BEG>
CommaSepatatedSqlHints(hints)
<COMMENT_END>
]
將生成如下代碼,在語(yǔ)法定義中使用正則 []
和 Token 來(lái)定義可選部分是怎么樣的呜舒,在 Parser 中先檢查下一個(gè) Token 類(lèi)型是否符合再調(diào)用相應(yīng)方法
// jj_ntk 表示 next token
// - 若為 -1,表示剛開(kāi)始遍歷 token袭蝗,往后移動(dòng)一位拿到第一個(gè) token
// - 若不為 -1,表是已經(jīng)拿到了 next token
// 總結(jié)來(lái)說(shuō) (jj_ntk==-1)?jj_ntk():jj_ntk 就是拿到下一個(gè) token
switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
// 如果下一個(gè) token 是 HINT_BEG到腥,則進(jìn)入相應(yīng)的分支流程
case HINT_BEG:
// 校驗(yàn) token 類(lèi)型
jj_consume_token(HINT_BEG);
CommaSepatatedSqlHints(hints);
// 校驗(yàn) token 類(lèi)型
jj_consume_token(COMMENT_END);
break;
default:
jj_la1[29] = jj_gen;
;
}
4.2蔚袍、0 次或 1 次
(
<STREAM> {
keywords.add(SqlSelectKeyword.STREAM.symbol(getPos()));
}
)?
將生成如下代碼,在語(yǔ)法中使用正則 (...)?
表示只出現(xiàn) 0 次或 1 次啤咽,在這一點(diǎn)上是和用 []
表示效果相同,我們看下面的 switch case
的實(shí)現(xiàn)也能驗(yàn)證這一點(diǎn)宇整。其余部分也是 Token 的校驗(yàn)和代碼調(diào)用
switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
case STREAM:
jj_consume_token(STREAM);
keywords.add(SqlSelectKeyword.STREAM.symbol(getPos()));
break;
default:
jj_la1[30] = jj_gen;
;
}
4.3、或邏輯
(
<DISTINCT> {
keywords.add(SqlSelectKeyword.DISTINCT.symbol(getPos()));
}
| <ALL> {
keywords.add(SqlSelectKeyword.ALL.symbol(getPos()));
}
)?
將生成如下代碼鳞青,在語(yǔ)法定義中:
- 使用
(...)?
來(lái)表示可選的,所以在生成的代碼中臂拓,使用CASE ALL: CASE DISTINCT
來(lái)表達(dá)可選- 下一個(gè) Token 是 ALL 或 DISTINCT 則進(jìn)入分支流程;否則進(jìn)入 default
- 在內(nèi)部埃儿,語(yǔ)法定義中使用 | 表示或邏輯,在生成的代碼中使用
switch童番、case
來(lái)表達(dá)
switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
case ALL:
case DISTINCT:
switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
case DISTINCT:
jj_consume_token(DISTINCT);
keywords.add(SqlSelectKeyword.DISTINCT.symbol(getPos()));
break;
case ALL:
jj_consume_token(ALL);
keywords.add(SqlSelectKeyword.ALL.symbol(getPos()));
break;
default:
jj_la1[31] = jj_gen;
jj_consume_token(-1);
throw new ParseException();
}
break;
default:
jj_la1[32] = jj_gen;
;
}
除了上面介紹的一些 pattern,還有更多的剃斧,但是基于上面介紹的,相信看懂其他的形式也不是問(wèn)題幼东,這里就不再一個(gè)個(gè)介紹了