从0到1实现一个简易的sqlite（一、REPL）

简介

REPL（Read-Eval-Print Loop）是一种交互式编程环境，它允许用户输入代码并立即查看结果。在本篇文章中，我们将介绍如何使用 Rust 编写一个简单的 REPL，它可以读取用户输入的代码并在控制台中打印出结果。

实现步骤

这个 REPL 主要分为 3 个步骤：读取用户输入、解析用户输入、执行用户输入。

读取用户输入

首先要循环读取用户的输入，当用户按下回车键后，我们就可以获取到用户在控制台中输入的内容了。如果用户没有输入任何内容，我们可以继续等待用户的输入。

解析用户输入

当我们获取到用户的输入后，就需要将其解析成程序可以理解的格式。我们首先判断用户输入的是不是元命令（以 "." 开头），如果是就执行相应的元命令。如果不是元命令，那么我们就需要解析出用户输入的 SQL 语句。

执行用户输入

当我们将用户输入的 SQL 语句解析出来后，就可以执行这个语句了。我们首先需要对输入的 SQL 语句进行预处理，然后执行这个语句。如果执行成功，我们就可以在控制台中打印出相应的结果。如果执行失败，我们就需要在控制台中打印出错误信息。

以上就是这个 REPL 的基本流程，如果你想了解更多细节，请查看代码。

代码

main.rs

Copy
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

Copy
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 foo@bar.com";
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, "foo@bar.com");
}
}

这是代码中的 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 字段，包括 id、username 和 email 三个字段。

在测试中，我们使用 test_prepare_insert 测试了 prepare_statement 函数是否能够正确解析 "insert" 语句，并且将解析出的值赋给 stmt 结构体的 row_to_insert 字段。

excutor.rs

Copy
#![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_insert 和 execute_select 函数都是未实现的占位函数，它们只是打印一条消息并返回一个错误。

Row 结构体表示一行数据，包含了 id、email 和 username 三个字段。Statement 结构体表示一个 SQL 语句，包含了要执行的语句类型和要插入的行数据。StatementType 枚举表示 SQL 语句的类型，包括 "insert" 和 "select" 两种类型。PrepareError 和 ExecuteError 枚举分别表示准备语句和执行语句时可能出现的错误，包括语句无法识别和参数数量不正确等。

在 Display trait 的实现中，我们为 PrepareError 和 ExecuteError 分别实现了 fmt 方法，用于格式化错误信息。

总结

本文主要介绍了 REPL 的基本流程和代码实现，包括解析用户输入的 SQL 语句和执行语句等。代码中的 parser.rs 文件包含了解析用户输入的函数和元命令的函数，excutor.rs 文件包含了执行用户输入的 SQL 语句的函数。