MySQL技術探索01實現SQL語法解析器
本文將介紹如何使用開源的語法和詞法分析框架bison和flex來實現SQL解析器。出於技術學習的目的,本文做描述的微型SQL解析器僅能實現對微型SQL的語法解析。
1.MySQL中的SQL解析器
包括JDBC、ODBC、ADO等等關系數據庫客戶端應用開發框架在內的各種SDK,核心功能是幫助程序員簡化各種客戶端的數據庫操作,同時將SQL語句通過網絡形式發送給MySQL等關系數據庫的服務器進程。MySQL服務器進行負責解析並執行這些SQL語句。SQL語句中的語法規則多種多樣,MySQL服務器是如何實現SQL解析的功能的呢? MySQL服務器代碼中有一個SQL解析器組件。本文將主要介紹跟語法分析和詞法分析相關的代碼自動生成開源框架,以及如何在這些框架中定義一套微型SQL解析器的詞法規則和語法規則,並在自動產生的代碼的基礎上開發一個微型SQL解析器軟件。
2.詞法解析器開源框架
詞法解析器的功能是接受輸入的文字,比如SQL腳本,根據一定的規則,將其轉化為一連串的標記(token)。人工編寫代碼實現這個功能也是可行的,但是隨著token的種類的增加和復雜程度的增加,這個編碼工作非常的繁瑣而且容易出錯。
詞法解析器框架的功能主要是根據自定義的詞法規則,自動產生相應的詞法解析器代碼。
本文使用的開源框架是flex。flex是apache旗下的開源軟件。使用flex,能夠極大的降低編寫詞法解析器代碼的工作量。
3.語法解析器開源框架
語法解析器的功能是接受輸入的標記(token)序列,根據一定的規則額,匹配預先定義的語句規則,將其識別為這些語句中的一個語句。同樣,人工編寫代碼實現這個功能不僅繁瑣而且容易出錯。
語法解析器框架的功能主要是根據自定義的語法規則,自動產生相應的語法解析器代碼。
本文使用的開源框架是bison。bison也是apache旗下的開源軟件。
4.微型SQL的例子
本文定義的微型SQL僅僅是標準SQL的一個很小的子集,僅僅提供建表、刪表,以及增刪改查操作的簡單語句。
支持的SQL語句的例子如下所示:
CREATE TABLE語句和DROP TABLE語句以及註釋。
目前暫不支持GROUP關鍵字,因此group暫時可以用作表名。
圖1
(2)INSERT INTO語句。
圖2
UPDATE語句。
圖3
DELETE語句。
圖4
SELECT語句。
支持多表JOIN,以及嵌套子查詢。
圖5
混合使用flex和bison後,可以分析出每一個語句中的每一個關鍵字和標識符。
5.微型SQL的語法規則
bison要求的語法規則定義文件的總體格式如下:
%option 這裏定義flex的一些選項,比如大小寫是否忽略等等。
%{
//此處定義需要使用的C++ 的.h文件的#include指令。
%}
//接著定義一些使用到的token。每一個token是一個關鍵字,或者是一個標識符。
%token
%token
%token
%%
//這裏定義語法規則。
%%
首先,為了滿足微型SQL的關鍵字忽略大小寫的要求等等,需要定義一些選項:
圖6
noyywrap:當flex掃描到文件結束符(EOF)時,不會自動調用yywrap()函數。
yywrap:與noyywrap相反。
case-insensitive:關鍵字忽略大小寫。SELECT和select以及Select都是合法關鍵字。
case-sensitive:與case-insensitive相反。
為了支持前面描述的微型SQL的解析工作,在my_sql_y.y文件中定義了如下的語法規則:
token列表。
圖7
總的SQL語法規則。
圖8
語法規則定義如下所示:
statement_comment:
expr_comment
{
}
;
expr_comment:
COMMENT_BEGIN comment_text COMMENT_END
{
}
;
comment_text:
COMMENT_TEXT
|comment_text COMMENT_TEXT
{
}
;
statement_create_table:
CREATE TABLE table_name BRACE_LEFT field_definition_list BRACE_RIGHT
{
my_string_list::iterator it = get_current_statement()->token_list.begin();
++it; ++it;
get_current_statement()->create_table.table_name = *it;
printf("create table :{%s}\r\n", it->c_str());
}
;
table_name:
IDENTIFIER
{
}
;
field_definition_list:
IDENTIFIER field_type
|field_definition_list SEPARATOR IDENTIFIER field_type
{
}
;
field_type:
FIELDTYPE_INT
|FIELDTYPE_VARCHAR BRACE_LEFT field_varchar_width BRACE_RIGHT
{
}
;
field_varchar_width:
NUMBER_INT
{
}
;
statement_drop_table:
DROP TABLE table_name
{
my_string_list::iterator it = get_current_statement()->token_list.begin();
++it; ++it;
get_current_statement()->drop_table.table_name = *it;
printf("drop table :{%s}\r\n", it->c_str());
}
;
statement_insert:
INSERT INTO table_name BRACE_LEFT field_name_list BRACE_RIGHT VALUES BRACE_LEFT field_value_list BRACE_RIGHT
{
}
;
field_name_list:
field_name
|field_name SEPARATOR field_name_list
{
}
;
field_value_list:
field_value
|field_value SEPARATOR field_value_list
{
}
;
field_value:
int_value
|varchar_value
{
}
;
int_value:
NUMBER_INT
{
}
;
varchar_value:
VALUE_SEPARATOR varchar_value_list VALUE_SEPARATOR
{
}
;
varchar_value_list:
VARCHAR_VALUE
|VARCHAR_VALUE varchar_value_list
{
}
;
field_name:
IDENTIFIER
{
}
;
statement_update:
UPDATE table_name SET field_assign_value_list where_clause
{
}
;
field_assign_value_list:
field_assign_value
|field_assign_value SEPARATOR field_assign_value_list
{
}
;
field_assign_value:
field_name EQUAL field_value
{
}
;
where_clause:
|WHERE where_condition_list
{
}
;
where_condition_list:
where_condition
|where_condition condition_logic where_condition_list
{
}
;
where_condition:
condition_clause
|condition_between
|BRACE_LEFT where_condition_list BRACE_RIGHT
{
}
;
condition_clause:
condition_value condition_compare condition_value
{
}
;
condition_value:
field_name
|field_value
|table_dot_field_name
{
}
;
condition_compare:
EQUAL
|LESS_THAN
|GREATER_THAN
|LIKE
{
}
;
condition_between:
field_name BETWEEN condition_value AND condition_value
{
}
;
condition_logic:
AND
|OR
{
}
;
statement_select:
statement_select_simple
{
}
;
statement_select_simple:
SELECT select_field_list from_clause join_clause where_clause orderby_clause
{
}
;
select_field_list:
STAR
|select_field_name
|select_field_name SEPARATOR select_field_list
{
}
;
select_field_name:
select_field
|select_field IDENTIFIER
|select_field AS IDENTIFIER
{
}
;
select_field:
field_name
|table_dot_field_name
{
}
;
orderby_clause:
|ORDER BY orderby_field_list
{
}
;
orderby_field_list:
orderby_field
|orderby_field SEPARATOR orderby_field_list
{
}
;
orderby_field:
orderby_field_name
|orderby_field_name ASC
|orderby_field_name DESC
{
}
;
orderby_field_name:
field_name
|table_dot_field_name
{
}
;
from_clause:
FROM from_clause_simple
|FROM from_clause_subquery_no_alias
|FROM from_clause_subquery_with_alias
{
}
;
from_clause_simple:
select_table_name
{
}
;
from_clause_subquery_no_alias:
BRACE_LEFT statement_select BRACE_RIGHT
{
}
;
from_clause_subquery_with_alias:
clause_subquery AS table_name
clause_subquery table_name
{
}
;
clause_subquery:
BRACE_LEFT statement_select BRACE_RIGHT
{
}
;
select_table_name:
table_name
|table_name AS table_name
|table_name table_name
{
}
;
join_clause:
|inner_join_clause join_condition_clause join_clause
|left_join_clause join_condition_clause join_clause
|right_join_clause join_condition_clause join_clause
{
}
;
join_condition_clause:
ON join_condition
{
}
;
join_condition:
table_dot_field_name condition_compare table_dot_field_name
{
}
;
table_dot_field_name:
TABLE_DOT_FIELD
{
}
;
inner_join_clause:
INNER JOIN join_table_name
{
}
;
left_join_clause:
LEFT JOIN join_table_name
|LEFT OUTER JOIN join_table_name
{
}
;
right_join_clause:
RIGHT JOIN join_table_name
|RIGHT OUTER JOIN join_table_name
{
}
;
join_table_name:
table_name
|table_name AS table_name
|table_name table_name
|BRACE_LEFT statement_select BRACE_RIGHT AS table_name
|BRACE_LEFT statement_select BRACE_RIGHT table_name
{
}
;
statement_delete:
DELETE FROM table_name where_clause
{
}
;
6.微型SQL的詞法規則
在my_sql_l.l文件中定義詞法規則,主要涉及用到的關鍵字和標識符。
(1)文件結構和my_sql_y.y文件類似。
(2)下面列出詞法規則中的所有關鍵字和標識符的定義。
%%
("CREATE"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("CREATE !");return CREATE;}
("DROP"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("DROP !");return DROP;}
("TABLE"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("TABLE !");return TABLE;}
[,]{1}{DO_TOKEN(GET_TOKEN());DEBUG_LOG("SEPARATOR !");return SEPARATOR;}
("INT"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("FIELDTYPE INT!");return FIELDTYPE_INT;}
(VARCHAR){DO_TOKEN(GET_TOKEN());DEBUG_LOG("FIELDTYPE VARCHAR !");return FIELDTYPE_VARCHAR;}
("/*"){is_in_comment++;;DEBUG_LOG("COMMENT_BEGIN!");return COMMENT_BEGIN;}
("*/"){is_in_comment--;DEBUG_LOG("COMMENT_END!");return COMMENT_END;}
[\(]{DO_TOKEN(GET_TOKEN());DEBUG_LOG("BRACE_LEFT");return BRACE_LEFT;}
[\)]{DO_TOKEN(GET_TOKEN());DEBUG_LOG("BRACE_RIGHT");return BRACE_RIGHT;}
[0-9]+{DO_TOKEN(GET_TOKEN());DEBUG_LOG("NUMBER_INT");return NUMBER_INT;}
";"{DO_TOKEN(GET_TOKEN());DEBUG_LOG("DELIMITER"); return DELIMITER;}
("INSERT"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("INSERT");return INSERT;}
("DELETE"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("DELETE");return DELETE;}
("UPDATE"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("UPDATE");return UPDATE;}
("SELECT"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("SELECT");return SELECT;}
("FROM"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("FROM");return FROM;}
("INTO"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("INTO");return INTO;}
("AS"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("AS");return AS;}
("WHERE"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("WHERE");return WHERE;}
("SET"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("SET");return SET;}
("VALUES"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("VALUES");return VALUES;}
("\*"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("STAR");return STAR;}
("\."){DO_TOKEN(GET_TOKEN());DEBUG_LOG("DOT");return DOT;}
("‘"){if( 0 ==is_in_comment ){is_in_value = ((++is_in_value)%2);} DO_TOKEN(GET_TOKEN());DEBUG_LOG("VALUE_SEPARATOR");return VALUE_SEPARATOR;}
("AND"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("AND");return AND;}
("OR"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("OR");return OR;}
("INNER"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("INNER");return INNER;}
("LEFT"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("LEFT");return LEFT;}
("RIGHT"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("RIGHT");return RIGHT;}
("JOIN"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("JOIN");return JOIN;}
("OUTER"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("OUTER");return OUTER;}
("ON"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("ON");return ON;}
("ORDER"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("ORDER");return ORDER;}
("BY"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("BY");return BY;}
("ASC"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("ASC");return ASC;}
("DESC"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("DESC");return DESC;}
("="){DO_TOKEN(GET_TOKEN());DEBUG_LOG("EQUAL");return EQUAL;}
("<"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("LESS_THAN");return LESS_THAN;}
(">"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("GREATER_THAN");return GREATER_THAN;}
("LIKE"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("LIKE");return LIKE;}
("BETWEEN"){DO_TOKEN(GET_TOKEN());DEBUG_LOG("BETWEEN");return BETWEEN;}
[A-Za-z0-9_]+{DO_TOKEN(GET_TOKEN());DEBUG_LOG("IDENTIFIER !");return IDENTIFIER;}
[A-Za-z0-9_]+\.[A-Za-z0-9]+{DO_TOKEN(GET_TOKEN());DEBUG_LOG("TABLE_DOT_FIELD !");return TABLE_DOT_FIELD;}
[A-Za-z0-9_@#/\\:&%!.*?+]+{DO_TOKEN(GET_TOKEN());DEBUG_LOG("VARCHAR_VALUE!");return VARCHAR_VALUE;}
[A-Za-z0-9_@#/\\:&^%$~!.*?+]+{DEBUG_LOG("COMMENT_TEXT!");return COMMENT_TEXT;}
%%
7.主程序說明
在主程序中調用yyparse()函數。
圖9
yyparse()函數啟動bison的語法分析過程,最終會調用yylex()函數識別出一個個的token,即關鍵字和標識符。由於這裏使用了flex,flex根據my_sql_l.l文件自動產生了yylex()函數的實現代碼,因此這個函數不需要人工編寫。
圖10
最後列出所使用的Makefile文件。
圖11
總結:
混合使用bison和flex的情況下,制作SQL解析器,需要以下幾個步驟:
(1)編寫my_sql_y.y文件。
這個文件定義語法規則,用於bison程序自動產生語法分析代碼的my_sql_y.cpp文件。
(2)編寫my_sql_l.l文件。
這個文件定義詞法規則,用於flex程序自動產生詞法分析代碼的my_sql_l.cpp文件,最主要是產生yylex()函數。
(3)使用bison和flex程序產生相應的cpp文件。
(4)編寫主程序代碼。
主要是啟動yyparse()函數開始語法分析工作。
8.運行測試結果
本文描述的方法用於準確的分析給出的SQL語句是否符合語法規則,對於不符合的給出語法錯誤的提示,對於符合規則的,給出符合哪一條規則,並準確解析出該SQL語句中的每一個關鍵字和標識符的值。
SELECT語句的分析:
原始SQL語句如下:
圖12
分析的結果中的幾個條目如下:
{SELECT}@c={0} @v={0}{SELECT}
說明:
第一個SELECT是識別出來的字符串。後面那個SELECT說明這個字符串是SELECT語句中的SELECT關鍵字。@c:是否為註釋,0表示不是註釋。@v:是否為字符串值,0表示不是字符串值。字符串值指的是INSERT/UPDATE/WHERE等中的VARCHAR(100)類型的字段的值,即單引號之類的字符串。
{p.id}@c={0} @v={0}{TABLE_DOT_FIELD !}
說明:
這個p.id是識別出來的字符串,TABLE_DOT_FIELD說明這是一個“表名.列名”標識符。
完整分析結果如下:
SELECT ... FROM部分的分析結果:
圖13
INNER JOIN和LEFT OUTER JOIN部分的分析結果,實現了三表JOIN的解析。
圖14
這是WHERE子句部分,以及語句結束後輸出的完整解析結果(即SQL的復原)。
語句以分號(;)作為語句結束符。
圖15
UPDATE語句的分析。
UPDATE語句如下:
圖16
分析結果如下:
圖17
目前的微型SQL語法解析器還是非常的簡陋。不管怎樣,這個微型SQL的語法解析器的原型已經完成了。
MySQL服務器中的SQL解析器除了SQL的語法解析工作之外,還做了其它一些更為復雜的工作。這些工作有待以後有機會再去繼續探索。
MySQL技術探索01實現SQL語法解析器