banner
orion

orion

中国科学技术大学研究生;数据库内核开发工程师;生产力爱好者;

從0到1實現一個簡易的sqlite(一、REPL)

簡介#

REPL(Read-Eval-Print Loop)是一種互動式編程環境,它允許用戶輸入代碼並立即查看結果。在本篇文章中,我們將介紹如何使用 Rust 編寫一個簡單的 REPL,它可以讀取用戶輸入的代碼並在控制台中打印出結果。

實現步驟#

這個 REPL 主要分為 3 個步驟:讀取用戶輸入、解析用戶輸入、執行用戶輸入。

  1. 讀取用戶輸入

    首先要循環讀取用戶的輸入,當用戶按下回車鍵後,我們就可以獲取到用戶在控制台中輸入的內容了。如果用戶沒有輸入任何內容,我們可以繼續等待用戶的輸入。

  2. 解析用戶輸入

    當我們獲取到用戶的輸入後,就需要將其解析成程序可以理解的格式。我們首先判斷用戶輸入的是不是元命令(以 "." 開頭),如果是就執行相應的元命令。如果不是元命令,那麼我們就需要解析出用戶輸入的 SQL 語句。

  3. 執行用戶輸入

    當我們將用戶輸入的 SQL 語句解析出來後,就可以執行這個語句了。我們首先需要對輸入的 SQL 語句進行預處理,然後執行這個語句。如果執行成功,我們就可以在控制台中打印出相應的結果。如果執行失敗,我們就需要在控制台中打印出錯誤信息。

以上就是這個 REPL 的基本流程,如果你想了解更多細節,請查看代碼。

代碼#

main.rs#

mod executor;
mod parser;
use executor::{execute_statement, PrepareError, Statement};
use parser::{do_meta_command, prepare_statement, MetaCommandResult};
use std::io::{self, Write};

fn main() -> io::Result<()> {
    loop {
        print_prompt();
        io::stdout().flush()?;
        let mut cmd = String::new();
        io::stdin().read_line(&mut cmd)?;
        if cmd.trim().is_empty() {
            continue;
        }

        if cmd.starts_with('.') {
            match do_meta_command(cmd.trim()) {
                MetaCommandResult::Exit => {
                    println!("exit, bye");
                    break;
                }
                MetaCommandResult::UnrecognizedCommand => {
                    println!("無法識別的命令 '{}'", cmd.trim());
                    continue;
                }
            };
        }

        let mut stmt: Statement = Statement::default();

        match prepare_statement(cmd.trim(), &mut stmt) {
            Ok(_) => {}

            Err(PrepareError::UnrecognizedStatement) => {
                println!("在'{}'的開頭無法識別的關鍵字", cmd.trim());
                continue;
            }
        };

        match execute_statement(&stmt) {
            Ok(_) => {
                println!("執行成功。");
            }
            Err(_) => {
                println!("執行語句時出錯");
                continue;
            }
        }
    }

    Ok(())
}

fn print_prompt() {
    print!("db > ")
}

prepare_statement 封裝到 parser.rs,是因為這個函數的作用是將用戶輸入的 SQL 語句解析成程序可以理解的格式,屬於語法分析的範疇,因此放在 parser.rs 文件中更為合適;而 execute_statement 函數則是將解析出來的語句進行執行,是需要執行器來完成的,因此將其封裝到 executor.rs 文件中。

parser.rs#

use crate::executor::{PrepareError, Statement, StatementType};
pub enum MetaCommandResult {
    Exit,
    UnrecognizedCommand,
}

pub fn do_meta_command(cmd: &str) -> MetaCommandResult {
    if cmd == ".exit" {
        MetaCommandResult::Exit
    } else {
        MetaCommandResult::UnrecognizedCommand
    }
}

pub fn prepare_statement(cmd: &str, stmt: &mut Statement) -> Result<(), PrepareError> {
    if cmd.starts_with("insert") {
        stmt.statement_type = StatementType::Insert;
        //將cmd解析成row
        let mut iter = cmd.split_whitespace();
        //判斷是否有三個參數
        if iter.clone().count() != 4 {
            return Err(PrepareError::UnrecognizedStatement);
        }
        iter.next();

        stmt.row_to_insert.id = iter.next().unwrap().parse::<u32>().unwrap();
        stmt.row_to_insert.username = iter.next().unwrap().to_string();
        stmt.row_to_insert.email = iter.next().unwrap().to_string();
        println!("stmt.row_to_insert: {:?}", stmt.row_to_insert);

        return Ok(());
    }
    if cmd.starts_with("select") {
        stmt.statement_type = StatementType::Select;
        return Ok(());
    }
    Err(PrepareError::UnrecognizedStatement)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_prepare_insert() {
        let mut stmt = Statement::default();
        let cmd = "insert 1 user1 [email protected]";
        let _result = prepare_statement(cmd, &mut stmt);
        if stmt.statement_type != StatementType::Insert {
            panic!("必須是插入語句");
        }

        //測試各個字段是否正確
        assert_eq!(stmt.row_to_insert.id, 1);
        assert_eq!(stmt.row_to_insert.username, "user1");
        assert_eq!(stmt.row_to_insert.email, "[email protected]");
    }
}

這是代碼中的 parse.rs 文件,它包含了解析用戶輸入的函數和元命令的函數。元命令是以 "." 開頭的命令,用於控制 REPL 的行為,如退出 REPL。

do_meta_command 函數用於解析元命令。如果用戶輸入的是 ".exit",函數返回 MetaCommandResult::Exit,表示需要退出 REPL;如果用戶輸入的是其他元命令,函數返回 MetaCommandResult::UnrecognizedCommand,表示無法識別的元命令。

prepare_statement 函數用於將用戶輸入的 SQL 語句解析成程序可以理解的格式。如果用戶輸入的是 "insert",函數將 SQL 語句解析成一個 Statement 結構體,包含了要插入的行的各個字段。如果用戶輸入的是 "select",函數將 SQL 語句解析成一個 Statement 結構體,不包含要查詢的條件。如果用戶輸入的是其他語句,函數返回 PrepareError::UnrecognizedStatement,表示無法識別的語句。

prepare_statement 函數中的代碼實現了解析 "insert" 語句的邏輯。首先將 Statement 結構體的 statement_type 字段設置為 StatementType::Insert,表示要插入一行數據。然後將 SQL 語句解析成一個迭代器,使用 split_whitespace 函數將其拆分成單詞。如果單詞數不是 4,函數返回 PrepareError::UnrecognizedStatement,表示無法識別的語句。否則,函數將解析出的值賦給 stmt 結構體的 row_to_insert 字段,包括 idusernameemail 三個字段。

在測試中,我們使用 test_prepare_insert 測試了 prepare_statement 函數是否能夠正確解析 "insert" 語句,並且將解析出的值賦給 stmt 結構體的 row_to_insert 字段。

excutor.rs#

#![allow(dead_code)]

use serde::{Deserialize, Serialize};
use std::fmt::Display;

#[derive(Debug, Eq, PartialEq)]
pub enum StatementType {
    Insert,
    Select,
    Unrecognized,
}

#[derive(Debug, Eq, PartialEq)]
pub enum PrepareError {
    UnrecognizedStatement,
    IncorrectParamNumber,
}
impl Display for PrepareError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PrepareError::UnrecognizedStatement => {
                write!(f, "無法識別的語句")
            }
            PrepareError::IncorrectParamNumber => {
                write!(f, "參數數量不正確")
            }
        }
    }
}

#[derive(Debug, Eq, PartialEq)]
pub enum ExecuteError {
    UnrecognizedStatement,
    Failure(String),
}

impl Display for ExecuteError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ExecuteError::UnrecognizedStatement => {
                write!(f, "無法識別的語句")
            }
            ExecuteError::Failure(s) => {
                write!(f, "失敗: {}", s)
            }
        }
    }
}

pub enum ExecuteResult {
    Record(Vec<Row>),
    Affected(u32),
}
pub struct Statement {
    pub row_to_insert: Row,
    pub statement_type: StatementType,
}

#[derive(Serialize, Deserialize, Default, Debug, Clone)]
pub struct Row {
    pub id: u32,
    pub email: String,
    pub username: String,
}
impl Default for Statement {
    fn default() -> Self {
        Statement {
            statement_type: StatementType::Unrecognized,
            row_to_insert: Row::default(),
        }
    }
}

pub fn execute_statement(stmt: &Statement) -> Result<ExecuteResult, ExecuteError> {
    match stmt.statement_type {
        StatementType::Insert => execute_insert(stmt),
        StatementType::Select => execute_select(stmt),
        StatementType::Unrecognized => Err(ExecuteError::UnrecognizedStatement),
    }
}

fn execute_insert(_stmt: &Statement) -> Result<ExecuteResult, ExecuteError> {
    println!("這是我們將執行插入的地方。");
    Err(ExecuteError::Failure("未實現".to_owned()))
}

fn execute_select(_stmt: &Statement) -> Result<ExecuteResult, ExecuteError> {
    println!("這是我們將執行查詢的地方。");
    Err(ExecuteError::Failure("未實現".to_owned()))
}

這是代碼中的 excutor.rs 文件,它包含了執行用戶輸入的 SQL 語句的函數。execute_statement 函數根據輸入的語句類型,調用相應的函數執行語句。如果語句類型是 "insert",函數調用 execute_insert 函數執行插入操作;如果語句類型是 "select",函數調用 execute_select 函數執行查詢操作;如果語句類型是其他值,函數返回 ExecuteError::UnrecognizedStatement,表示無法識別的語句類型。

execute_insertexecute_select 函數都是未實現的佔位函數,它們只是打印一條消息並返回一個錯誤。

Row 結構體表示一行數據,包含了 idemailusername 三個字段。Statement 結構體表示一個 SQL 語句,包含了要執行的語句類型和要插入的行數據。StatementType 枚舉表示 SQL 語句的類型,包括 "insert" 和 "select" 兩種類型。PrepareErrorExecuteError 枚舉分別表示準備語句和執行語句時可能出現的錯誤,包括語句無法識別和參數數量不正確等。

Display trait 的實現中,我們為 PrepareErrorExecuteError 分別實現了 fmt 方法,用於格式化錯誤信息。

總結#

本文主要介紹了 REPL 的基本流程和代碼實現,包括解析用戶輸入的 SQL 語句和執行語句等。代碼中的 parser.rs 文件包含了解析用戶輸入的函數和元命令的函數,excutor.rs 文件包含了執行用戶輸入的 SQL 語句的函數。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。