banner
orion

orion

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

0から1まで簡易的なsqliteを実現する(一、REPL)

概要#

REPL(Read-Eval-Print Loop)は、ユーザーがコードを入力し、すぐに結果を確認できるインタラクティブなプログラミング環境です。本記事では、Rust を使用して、ユーザーが入力したコードを読み取り、コンソールに結果を表示するシンプルな REPL を作成する方法を紹介します。

実装手順#

この REPL は主に 3 つのステップに分かれています:ユーザー入力の読み取り、ユーザー入力の解析、ユーザー入力の実行。

  1. ユーザー入力の読み取り

    まず、ユーザーの入力をループで読み取ります。ユーザーが Enter キーを押すと、コンソールに入力された内容を取得できます。ユーザーが何も入力しなかった場合は、引き続きユーザーの入力を待ちます。

  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_statementparser.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();
        //3つのパラメータがあるか確認
        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!("必ずinsert文である必要があります");
        }

        //各フィールドが正しいかテスト
        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]");
    }
}

これはコード内の parser.rs ファイルで、ユーザー入力を解析する関数とメタコマンドの関数が含まれています。メタコマンドは "." で始まるコマンドで、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 フィールドに割り当てます。

テストでは、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 の 3 つのフィールドを含みます。Statement 構造体は SQL 文を表し、実行する文のタイプと挿入する行データを含みます。StatementType 列挙型は SQL 文のタイプを示し、"insert" と "select" の 2 種類があります。PrepareErrorExecuteError 列挙型は、それぞれ準備文と実行文の際に発生する可能性のあるエラーを示します。

Display トレイトの実装では、PrepareErrorExecuteError に対してそれぞれ fmt メソッドを実装し、エラーメッセージをフォーマットしています。

まとめ#

本記事では、REPL の基本的な流れとコード実装について紹介しました。ユーザー入力の SQL 文を解析し、文を実行することを含みます。コード内の parser.rs ファイルにはユーザー入力を解析する関数とメタコマンドの関数が含まれ、excutor.rs ファイルにはユーザー入力の SQL 文を実行する関数が含まれています。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。