Belajar Membuat Database Sederhana dengan Bahasa C Bagian 2

Bagus Aji Santoso 2 Oktober 2017

Belajar Membuat Database Sederhana dengan Bahasa C Bagian 2

Artikel ini merupakan terjemahan dari seri Let's Build a Simple Database yang ditulis oleh Connor Stack Baca bagian pertama disini

Kita sedang membuat sebuah tiruan sqlite. Bagian "front-end" dari sqlite adalah sebuah kompiler SQL yang membaca sebuah string dan mencetak representasi data internal yang disebut bytecode.

Bytecode ini dikirim ke virtual machine yang kemudian akan mengeksekusinya.

Image

Membagi proses menjadi dua langkah seperti yang telah dijelaskan di atas memiliki beberapa keuntungan:

  • Mengurangi kompleksitas tiap bagian (contoh: virtual machine tidak perlu memikirkan kesalahan sintaks)
  • Dapat menyimpan cache bytecode agar dapat dipakai ulang sehingga meningkatkan performa.

Sekarang mari kita refactor fungsi main untuk mendukung dua keyword baru (tanda - artinya bagian kode di hapus dan tanda + artinya bagian ditambahkan:

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"); } }

Perintah-perintah non SQL seperti .exit disebut "meta-commands". Perintah-perintah ini dimulai dengan sebuah titik, jadi kita dapat memeriksa dan menanganinya di fungsi yang terpisah.

Selanjutnya, kita menambahkan kode yang akan mengubah baris input menjadi representasi dari sebuah perintah. Ini lah versi front-end sqlite yang kita buat sendiri.

Terakhir, kita mengirimkan perintah yang telah dibuat tadi ke execute_statement. Fungsi ini lah yang akan menjadi virtual machine.

Perhatiakn dua funngsi baru kita mengembalikan sebuah enum yang mengindikasikan suatu perintah berhasil atau gagal:

enum MetaCommandResult_t { META_COMMAND_SUCCESS, META_COMMAND_UNRECOGNIZED_COMMAND }; typedef enum MetaCommandResult_t MetaCommandResult; enum PrepareResult_t { PREPARE_SUCCESS, PREPARE_UNRECOGNIZED_STATEMENT }; typedef enum PrepareResult_t PrepareResult;

"Unrecognized statement"? Hasil itu seperti sebuah exception. Namun, exception itu kurang baik (dan C bahkan tidak punya dukungan untuk fitur tersebut), jadi di sini saya menggunakan kode hasil enum saat diperlukan. Kompiler C akan komplain jika perintah switch tidak menangani suatu enum, jadi akan lebih baik jika kita menangani tiap hasil dari sebuah fungsi. Nanti kita akan menambahkan lebih banyak kode hasil.

Fungsi do_meta_command hanyalah sebuah wrapper untuk fungsi yang sudah ada yang dapat dimanfaatkan untuk perintah lain:

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" kita saat ini hanya memiliki sebuah enum dengan dua kemungkinan nilai. Enum ini akan memiliki lebih banyak data saat kita menambahkan lebih banyak parameter di kodenya:

enum StatementType_t { STATEMENT_INSERT, STATEMENT_SELECT }; typedef enum StatementType_t StatementType; struct Statement_t { StatementType type; }; typedef struct Statement_t Statement;

Fungsi prepare_statement (“SQL Compiler” kita) saat ini belum mengerti bahasa SQL. Bahkan dia hanya mengerti dua kata:

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; }

Perhatikan bahwa kita menggunakan strncmp untuk "insert" karena perintah "insert" akan diikuti oleh data. (contoh insert 1 cstack foo@bar.com).

Terakhir, execute_statement memiliki beberapa bagian:

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; } }

Dapat dilihat bahwa belum ada kode error yang dikembalikan karena belum ada hal yang dapat menyebabkan error terjadi.

Dengan refactor yang telah kita lakukan, kini kita aplikasi kita dapat mengenal dua perintah baru!

~ ./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
~

Kerangka database kita sudah mulai terlihat... bukankah lebih kerena jika ia bisa menyimpan data? Dibagian berikutnya, kita akan mengimplementasi insert dan select, membuat tempat penyimpanan data paling buruk di dunia.

Berikut diff keseluruhan untuk perubahan di bagian kedua dari bagian pertama:

@@ -10,6 +10,23 @@ struct InputBuffer_t { }; typedef struct InputBuffer_t InputBuffer; +enum MetaCommandResult_t { + META_COMMAND_SUCCESS, + META_COMMAND_UNRECOGNIZED_COMMAND +}; +typedef enum MetaCommandResult_t MetaCommandResult; + +enum PrepareResult_t { PREPARE_SUCCESS, PREPARE_UNRECOGNIZED_STATEMENT }; +typedef enum PrepareResult_t PrepareResult; + +enum StatementType_t { STATEMENT_INSERT, STATEMENT_SELECT }; +typedef enum StatementType_t StatementType; + +struct Statement_t { + StatementType type; +}; +typedef struct Statement_t Statement; + InputBuffer* new_input_buffer() { InputBuffer* input_buffer = malloc(sizeof(InputBuffer)); input_buffer->buffer = NULL; @@ -35,16 +52,66 @@ void read_input(InputBuffer* input_buffer) { input_buffer->buffer[bytes_read - 1] = 0; } +MetaCommandResult do_meta_command(InputBuffer* input_buffer) { + if (strcmp(input_buffer->buffer, ".exit") == 0) { + 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) { - 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"); } }