前文回顧:實現一個簡單的Database1(譯文) 譯註:cstsck在github維護了一個簡單的、類似sqlite的資料庫實現,通過這個簡單的項目,可以很好的理解資料庫是如何運行的。本文是第二篇,主要是實現資料庫的前端組件,編譯器與虛擬機部分功能 Part 2 世界上最簡單的SQL編譯器與虛擬機 ...
前文回顧:實現一個簡單的Database1(譯文)
譯註:cstsck在github維護了一個簡單的、類似sqlite的資料庫實現,通過這個簡單的項目,可以很好的理解資料庫是如何運行的。本文是第二篇,主要是實現資料庫的前端組件,編譯器與虛擬機部分功能
Part 2 世界上最簡單的SQL編譯器與虛擬機
我們正在實現一個sqlite的克隆版本。sqlite的前端是SQL編譯器,編譯器用來解析字元串並輸出一個內部的表示,叫做位元組碼。
這些位元組碼被傳到虛擬機(virtual machine),在虛擬機中,位元組碼將被執行。
SQLite Architecture (https://www.sqlite.org/arch.html)
像這樣把事情分成兩個步驟(SQL編譯和虛擬機)有以下兩個優點:
- 減少各個部分的複雜性(例如:虛擬機不用關心輸入語句語法錯誤)
- 允許只編譯通用查詢一次,然後對生成的位元組碼進行緩存,以此來提升性能
有了這些想法,讓我們來重構主函數,在程式中支持了兩個新的關鍵字:
譯註:下麵代碼中行開頭加減號是相對與第一部分(part 1)的實現,增加或者刪除的代碼。代碼對main()重構以適合識別新關鍵字,在第一部分中,main()函數只能識別“.exit”關鍵字,也就是程式退出命令。
int main(int argc, char* argv[]) {
InputBuffer* input_buffer = new_input_buffer();
while (true) {
print_prompt();
read_input(input_buffer);
- if (strcmp(input_buffer->buffer, ".exit") == 0) {
- exit(EXIT_SUCCESS);
- } else {
- printf("Unrecognized command '%s'.\n", input_buffer->buffer);
+ if (input_buffer->buffer[0] == '.') {
+ switch (do_meta_command(input_buffer)) {
+ case (META_COMMAND_SUCCESS):
+ continue;
+ case (META_COMMAND_UNRECOGNIZED_COMMAND):
+ printf("Unrecognized command '%s'\n", input_buffer->buffer);
+ continue;
+ }
}
+
+ Statement statement;
+ switch (prepare_statement(input_buffer, &statement)) {
+ case (PREPARE_SUCCESS):
+ break;
+ case (PREPARE_UNRECOGNIZED_STATEMENT):
+ printf("Unrecognized keyword at start of '%s'.\n",
+ input_buffer->buffer);
+ continue;
+ }
+
+ execute_statement(&statement);
+ printf("Executed.\n");
}
}
非SQL語句,像“.exit”這樣的命令被稱為“meta-commands”。它們都是以“.”開頭,所以我們在一個獨立的函數中檢查並且處理它們。
譯註:在上邊代碼中使用了單獨的if+switch來處理了以“.”開頭的“meta-commands”。
接下來,增加一個步驟,將輸入行命令轉換成內部表示的語句。這是sqlite前端的一個破解版本。
最後,我門將預編譯語句傳遞到execute_statement()函數,這個函數將最終變成我們的虛擬機。
註意我們的兩個新函數返回enum(枚舉)類型的來表示成功或者失敗:
typedef enum {
META_COMMAND_SUCCESS,
META_COMMAND_UNRECOGNIZED_COMMAND
} MetaCommandResult;
typedef enum { PREPARE_SUCCESS, PREPARE_UNRECOGNIZED_STATEMENT } PrepareResult;
在輸入命令行語句無法識別時,列印“Unrecognized statement”輸出?這個看起來像是異常(exception)。我不喜歡使用exception(並且C語言甚至不支持exception),所以我在任何可行的地方都是用enum結果碼做返回。如果我的switch語句沒有處理enum成員,C編譯器會報錯,所以我們能感到小有信心,我們能處理所有函數結果。預計將來會有更多的結果代碼被加入。
do_meta_command()函數只是對已有的功能的一個封裝,為更多的命令留出空間:
MetaCommandResult do_meta_command(InputBuffer* input_buffer) {
if (strcmp(input_buffer->buffer, ".exit") == 0) {
exit(EXIT_SUCCESS);
} else {
return META_COMMAND_UNRECOGNIZED_COMMAND;
}
}
我們的“prepared statement”現在只包含一個enum(有兩個可能值)。在語句中將會包含更多的我們允許的參數數據:
typedef enum { STATEMENT_INSERT, STATEMENT_SELECT } StatementType;
typedef struct {
StatementType type;
} Statement;
prepare_statement()函數(我們的SQL編譯器)現在還不能理解SQL。事實上,它現在只能理解兩個單詞:
譯註:下麵的代碼實現了對insert和select關鍵的解析。
PrepareResult prepare_statement(InputBuffer* input_buffer,
Statement* statement) {
if (strncmp(input_buffer->buffer, "insert", 6) == 0) {
statement->type = STATEMENT_INSERT;
return PREPARE_SUCCESS;
}
if (strcmp(input_buffer->buffer, "select") == 0) {
statement->type = STATEMENT_SELECT;
return PREPARE_SUCCESS;
}
return PREPARE_UNRECOGNIZED_STATEMENT;
}
註意,因為“insert”關鍵字後面有跟隨數據,所以為“insert”使用了strncmp()庫函數來比對輸入值。(例如輸入語句為:insert 1 cstack [email protected])
譯註:C 庫函數 int strncmp(const char *str1, const char *str2, size_t n) 是把輸入參數 str1 和 str2 進行比較,最多比較入參的前 n 個位元組。
最後,execute_statement()函數中包含了一些樁(stubs):
譯註:stubs(一小塊代碼),是為了實現測試代碼進行,會硬編碼一些輸入和輸出,即在execute_statement()函數中對prepare_statement()函數處理結果進行了引用並處理。
void execute_statement(Statement* statement) {
switch (statement->type) {
case (STATEMENT_INSERT):
printf("This is where we would do an insert.\n");
break;
case (STATEMENT_SELECT):
printf("This is where we would do a select.\n");
break;
}
}
註意這裡沒有返回任何錯誤碼,這是因為在這裡還不會有任何報錯發生。
譯註:目前為止,程式可解析“.exit”、“insert xxx”、"select xxx"命令,其餘不會識別,只輸出“Unrecognized command 'xxx'”,所以不會有什麼報錯輸出。參考下麵的演示。
做了這些重構後,我們的程式就能識別兩個新的關鍵字了。
~ ./db
db > insert foo bar
This is where we would do an insert.
Executed.
db > delete foo
Unrecognized keyword at start of 'delete foo'.
db > select
This is where we would do a select.
Executed.
db > .tables
Unrecognized command '.tables'
db > .exit
~
我們的資料庫骨架正在形成...如果它能存儲數據不是很好嗎?在下一部分,我們會實現insert和select,創建世界上最差勁的數據存儲。
同時,下麵是這部分重構的整個代碼不同之處:
@@ -10,6 +10,23 @@ struct InputBuffer_t {
} InputBuffer;
+typedef enum {
+ META_COMMAND_SUCCESS,
+ META_COMMAND_UNRECOGNIZED_COMMAND
+} MetaCommandResult;
+
+typedef enum { PREPARE_SUCCESS, PREPARE_UNRECOGNIZED_STATEMENT } PrepareResult;
+
+typedef enum { STATEMENT_INSERT, STATEMENT_SELECT } StatementType;
+
+typedef struct {
+ StatementType type;
+} Statement;
+
InputBuffer* new_input_buffer() {
InputBuffer* input_buffer = malloc(sizeof(InputBuffer));
input_buffer->buffer = NULL;
@@ -40,17 +57,67 @@ void close_input_buffer(InputBuffer* input_buffer) {
free(input_buffer);
}
+MetaCommandResult do_meta_command(InputBuffer* input_buffer) {
+ if (strcmp(input_buffer->buffer, ".exit") == 0) {
+ close_input_buffer(input_buffer);
+ exit(EXIT_SUCCESS);
+ } else {
+ return META_COMMAND_UNRECOGNIZED_COMMAND;
+ }
+}
+
+PrepareResult prepare_statement(InputBuffer* input_buffer,
+ Statement* statement) {
+ if (strncmp(input_buffer->buffer, "insert", 6) == 0) {
+ statement->type = STATEMENT_INSERT;
+ return PREPARE_SUCCESS;
+ }
+ if (strcmp(input_buffer->buffer, "select") == 0) {
+ statement->type = STATEMENT_SELECT;
+ return PREPARE_SUCCESS;
+ }
+
+ return PREPARE_UNRECOGNIZED_STATEMENT;
+}
+
+void execute_statement(Statement* statement) {
+ switch (statement->type) {
+ case (STATEMENT_INSERT):
+ printf("This is where we would do an insert.\n");
+ break;
+ case (STATEMENT_SELECT):
+ printf("This is where we would do a select.\n");
+ break;
+ }
+}
+
int main(int argc, char* argv[]) {
InputBuffer* input_buffer = new_input_buffer();
while (true) {
print_prompt();
read_input(input_buffer);
- if (strcmp(input_buffer->buffer, ".exit") == 0) {
- close_input_buffer(input_buffer);
- exit(EXIT_SUCCESS);
- } else {
- printf("Unrecognized command '%s'.\n", input_buffer->buffer);
+ if (input_buffer->buffer[0] == '.') {
+ switch (do_meta_command(input_buffer)) {
+ case (META_COMMAND_SUCCESS):
+ continue;
+ case (META_COMMAND_UNRECOGNIZED_COMMAND):
+ printf("Unrecognized command '%s'\n", input_buffer->buffer);
+ continue;
+ }
}
+
+ Statement statement;
+ switch (prepare_statement(input_buffer, &statement)) {
+ case (PREPARE_SUCCESS):
+ break;
+ case (PREPARE_UNRECOGNIZED_STATEMENT):
+ printf("Unrecognized keyword at start of '%s'.\n",
+ input_buffer->buffer);
+ continue;
+ }
+
+ execute_statement(&statement);
+ printf("Executed.\n");
}
}
Enjoy GreatSQL