diff --git a/grammar.ebnf b/grammar.ebnf index 4fefa7b..60e3641 100644 --- a/grammar.ebnf +++ b/grammar.ebnf @@ -1,3 +1,4 @@ +/* token is first stage of parsing */ token ::= insert | select | meta-command @@ -5,6 +6,12 @@ token ::= insert | string | end-of-file +/* command is second stage of parsing */ +command ::= cmd-insert + | cmd-select +cmd-insert ::= insert int string string +cmd-select ::= select + insert ::= "insert" select ::= "select" diff --git a/notes.org b/notes.org index c03ef68..223e8e9 100644 --- a/notes.org +++ b/notes.org @@ -235,6 +235,8 @@ i will use rustyline, since it seems like the most feature-complete insert ** TODO Row struct ** TODO parse row insert +** TODO separate statements with semicolons +** TODO in case of parse error, skip until next semicolon to better recover ** TODO serialize/deserialize row to/from raw bytes *** TODO look for best practices for creating binary formats diff --git a/src/command.rs b/src/command.rs index 75c2792..d19aeaa 100644 --- a/src/command.rs +++ b/src/command.rs @@ -80,7 +80,12 @@ mod tests { #[test] fn test_execute_insert_statement() { - let statement: Command = Statement::Insert.into(); + let statement: Command = Statement::Insert { + id: 45, + username: String::from("user"), + email: String::from("user@example.org"), + } + .into(); let result = statement.execute().display(); assert_snapshot!(result); } diff --git a/src/meta_commands.rs b/src/meta_commands.rs index 1bcc2b9..135e1ab 100644 --- a/src/meta_commands.rs +++ b/src/meta_commands.rs @@ -26,13 +26,13 @@ impl MetaCommand { match self { MetaCommand::Exit => MetaCommandExecuteResult { should_exit: true }, MetaCommand::About => { - print!("{}", branding::about_msg()); - MetaCommandExecuteResult { should_exit: false } - } + print!("{}", branding::about_msg()); + MetaCommandExecuteResult { should_exit: false } + } MetaCommand::Version => { print!("{}", branding::version_msg()); - MetaCommandExecuteResult { should_exit: false } - }, + MetaCommandExecuteResult { should_exit: false } + } } } } diff --git a/src/parser.rs b/src/parser.rs index 7cff3b0..bd6b087 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3,7 +3,7 @@ use std::collections::VecDeque; use crate::{ command::{Command, CommandParseError}, statements::Statement, - tokens::tokenize, + tokens::{Location, Token, TokenData, tokenize}, }; pub fn parse(file: String, input: String) -> Result, Vec> { @@ -14,7 +14,10 @@ pub fn parse(file: String, input: String) -> Result, Vec cmds.push(Command::Statement(Statement::Insert)), + crate::tokens::TokenData::Insert => match parse_insert_command(&mut tokens) { + Ok(cmd) => cmds.push(cmd), + Err(err) => errs.push(err), + }, crate::tokens::TokenData::Select => cmds.push(Command::Statement(Statement::Select)), crate::tokens::TokenData::MetaCommand(meta_command) => { cmds.push(Command::MetaCommand(meta_command)) @@ -33,6 +36,83 @@ pub fn parse(file: String, input: String) -> Result, Vec, +) -> Result { + // According to grammar.ebnf, insert command should be: insert int string string + + // Parse the id (integer) + let id_token = tokens.pop_front().ok_or_else(|| { + CommandParseError::UnexpectedToken( + Token { + location: tokens.back().map_or_else( + || Location::new(String::from(""), 0, 0), + |t| t.location.clone(), + ), + data: TokenData::EndOfFile, + lexeme: String::new(), + }, + &["integer"], + ) + })?; + + let id = match id_token.data { + TokenData::Int(id) => id, + _ => return Err(CommandParseError::UnexpectedToken(id_token, &["integer"])), + }; + + // Parse the username (string) + let username_token = tokens.pop_front().ok_or_else(|| { + CommandParseError::UnexpectedToken( + Token { + location: tokens.back().map_or_else( + || Location::new(String::from(""), 0, 0), + |t| t.location.clone(), + ), + data: TokenData::EndOfFile, + lexeme: String::new(), + }, + &["string"], + ) + })?; + + let username = match username_token.data { + TokenData::String(username) => username, + _ => { + return Err(CommandParseError::UnexpectedToken( + username_token, + &["string"], + )); + } + }; + + // Parse the email (string) + let email_token = tokens.pop_front().ok_or_else(|| { + CommandParseError::UnexpectedToken( + Token { + location: tokens.back().map_or_else( + || Location::new(String::from(""), 0, 0), + |t| t.location.clone(), + ), + data: TokenData::EndOfFile, + lexeme: String::new(), + }, + &["string"], + ) + })?; + + let email = match email_token.data { + TokenData::String(email) => email, + _ => return Err(CommandParseError::UnexpectedToken(email_token, &["string"])), + }; + + Ok(Command::Statement(Statement::Insert { + id, + username, + email, + })) +} + #[cfg(test)] mod tests { use super::*; @@ -44,8 +124,20 @@ mod tests { assert_debug_snapshot!(parse(file.clone(), String::from(".exit"))); assert_debug_snapshot!(parse(file.clone(), String::from("select"))); assert_debug_snapshot!(parse(file.clone(), String::from("sElEcT"))); - assert_debug_snapshot!(parse(file.clone(), String::from("INSERT"))); - assert_debug_snapshot!(parse(file.clone(), String::from("InSErT"))); + } + + #[test] + fn test_parse_insert_command() { + let file = String::from(""); + assert_debug_snapshot!(parse( + file.clone(), + String::from(r#"insert 1 "username" "email@example.com""#) + )); + assert_debug_snapshot!(parse( + file.clone(), + String::from(r#"insert "not_an_id" "username" "email@example.com""#) + )); + assert_debug_snapshot!(parse(file.clone(), String::from(r#"insert 1 "username""#))); } #[test] @@ -63,7 +155,7 @@ mod tests { let file = String::from(""); assert_debug_snapshot!(parse( file.clone(), - String::from(".exit select select insert select") + String::from(".exit select select select") )); } diff --git a/src/snapshots/osdb__command__tests__execute_insert_statement.snap b/src/snapshots/osdb__command__tests__execute_insert_statement.snap index 89809d2..aaea9a0 100644 --- a/src/snapshots/osdb__command__tests__execute_insert_statement.snap +++ b/src/snapshots/osdb__command__tests__execute_insert_statement.snap @@ -2,4 +2,4 @@ source: src/command.rs expression: result --- -insert +insert 45 "user" "user@example.org" diff --git a/src/snapshots/osdb__parser__tests__parse_insert_command-2.snap b/src/snapshots/osdb__parser__tests__parse_insert_command-2.snap new file mode 100644 index 0000000..8582c85 --- /dev/null +++ b/src/snapshots/osdb__parser__tests__parse_insert_command-2.snap @@ -0,0 +1,60 @@ +--- +source: src/parser.rs +expression: "parse(file.clone(),\nString::from(r#\"insert \"not_an_id\" \"username\" \"email@example.com\"\"#))" +--- +Err( + [ + UnexpectedToken( + Token { + location: Location { + file: "", + offset: 7, + length: 11, + }, + data: String( + "not_an_id", + ), + lexeme: "\"not_an_id\"", + }, + [ + "integer", + ], + ), + UnexpectedToken( + Token { + location: Location { + file: "", + offset: 19, + length: 10, + }, + data: String( + "username", + ), + lexeme: "\"username\"", + }, + [ + "statement", + "meta command", + "eof", + ], + ), + UnexpectedToken( + Token { + location: Location { + file: "", + offset: 30, + length: 19, + }, + data: String( + "email@example.com", + ), + lexeme: "\"email@example.com\"", + }, + [ + "statement", + "meta command", + "eof", + ], + ), + ], +) diff --git a/src/snapshots/osdb__parser__tests__parse_insert_command-3.snap b/src/snapshots/osdb__parser__tests__parse_insert_command-3.snap new file mode 100644 index 0000000..dc13e7a --- /dev/null +++ b/src/snapshots/osdb__parser__tests__parse_insert_command-3.snap @@ -0,0 +1,22 @@ +--- +source: src/parser.rs +expression: "parse(file.clone(), String::from(r#\"insert 1 \"username\"\"#))" +--- +Err( + [ + UnexpectedToken( + Token { + location: Location { + file: "", + offset: 19, + length: 0, + }, + data: EndOfFile, + lexeme: "", + }, + [ + "string", + ], + ), + ], +) diff --git a/src/snapshots/osdb__parser__tests__parse_insert_command.snap b/src/snapshots/osdb__parser__tests__parse_insert_command.snap new file mode 100644 index 0000000..954c83e --- /dev/null +++ b/src/snapshots/osdb__parser__tests__parse_insert_command.snap @@ -0,0 +1,15 @@ +--- +source: src/parser.rs +expression: "parse(file.clone(),\nString::from(r#\"insert 1 \"username\" \"email@example.com\"\"#))" +--- +Ok( + [ + Statement( + Insert { + id: 1, + username: "username", + email: "email@example.com", + }, + ), + ], +) diff --git a/src/snapshots/osdb__parser__tests__parse_multiple_correct.snap b/src/snapshots/osdb__parser__tests__parse_multiple_correct.snap index a90d0ae..4642731 100644 --- a/src/snapshots/osdb__parser__tests__parse_multiple_correct.snap +++ b/src/snapshots/osdb__parser__tests__parse_multiple_correct.snap @@ -1,6 +1,6 @@ --- source: src/parser.rs -expression: "parse(file.clone(), String::from(\".exit select select insert select\"))" +expression: "parse(file.clone(), String::from(\".exit select select select\"))" --- Ok( [ @@ -13,9 +13,6 @@ Ok( Statement( Select, ), - Statement( - Insert, - ), Statement( Select, ), diff --git a/src/statements.rs b/src/statements.rs index 5ba1103..3799caf 100644 --- a/src/statements.rs +++ b/src/statements.rs @@ -1,6 +1,10 @@ #[derive(Debug)] pub enum Statement { - Insert, + Insert { + id: i64, + username: String, + email: String, + }, Select, } @@ -11,8 +15,12 @@ pub struct StatementExecuteResult { impl Statement { pub fn execute(&self) -> StatementExecuteResult { match self { - Statement::Insert => StatementExecuteResult { - msg: String::from("insert"), + Statement::Insert { + id, + username, + email, + } => StatementExecuteResult { + msg: String::from(format!("insert {id:?} {username:?} {email:?}")), }, Statement::Select => StatementExecuteResult { msg: String::from("select"), diff --git a/src/tokens.rs b/src/tokens.rs index a1a850b..14316a2 100644 --- a/src/tokens.rs +++ b/src/tokens.rs @@ -10,7 +10,7 @@ pub enum TokenData { String(String), } -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, Clone)] pub struct Location { /// file name pub file: String,