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!("Unrecognized command '{}'", cmd.trim());
                    continue;
                }
            };
        }

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

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

            Err(PrepareError::UnrecognizedStatement) => {
                println!("Unrecognized keyword at start of '{}'", cmd.trim());
                continue;
            }
        };

        match execute_statement(&stmt) {
            Ok(_) => {
                println!("Executed.");
            }
            Err(_) => {
                println!("Error executing statement");
                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!("must be insert statement");
        }

        //测试各个字段是否正确
        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, "UnrecognizedStatement")
            }
            PrepareError::IncorrectParamNumber => {
                write!(f, "IncorrectParamNumber")
            }
        }
    }
}

#[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, "UnrecognizedStatement")
            }
            ExecuteError::Failure(s) => {
                write!(f, "Failure: {}", 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!("This is where we would do an insert.");
    Err(ExecuteError::Failure("unimplemented".to_owned()))
}

fn execute_select(_stmt: &Statement) -> Result<ExecuteResult, ExecuteError> {
    println!("This is where we would do a select.");
    Err(ExecuteError::Failure("unimplemented".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 语句的函数。

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。