diff --git a/grammar.ebnf b/grammar.ebnf index e979f99..ff4bd2e 100644 --- a/grammar.ebnf +++ b/grammar.ebnf @@ -1,32 +1,43 @@ -token ::= insert - | select - | meta-command - | int - | string - | end-of-file +/* token is first stage of parsing */ +token ::= insert + | select + | meta-command + | int + | string + | semicolon + | end-of-file -insert ::= "insert" -select ::= "select" +/* command is second stage of parsing */ +command ::= cmd-insert semicolon + | cmd-select semicolon +cmd-insert ::= insert int string string +cmd-select ::= select -meta-command ::= "." "exit" - | "about" +insert ::= "insert" +select ::= "select" +semicolon ::= ";" -int ::= sign? digit+ -sign ::= "+" - | "-" -digit ::= "0" - | "1" - | "2" - | "3" - | "4" - | "5" - | "6" - | "7" - | "8" - | "9" +meta-command ::= meta-command-verb end-of-file +meta-command-verb ::= ".exit" + | ".about" + | ".version" -string ::= '"' string-char* '"' -string-char ::= '\' utf8-char - | utf8-char-not-dbl-quote +int ::= sign? digit+ +sign ::= "+" + | "-" +digit ::= "0" + | "1" + | "2" + | "3" + | "4" + | "5" + | "6" + | "7" + | "8" + | "9" + +string ::= '"' string-char* '"' +string-char ::= '\' utf8-char + | utf8-char-not-dbl-quote diff --git a/notes.org b/notes.org index 76423a1..bf78fcf 100644 --- a/notes.org +++ b/notes.org @@ -226,17 +226,53 @@ i will use rustyline, since it seems like the most feature-complete * DONE write a proper grammar -* TODO .about meta-command -* TODO .version meta-command +* DONE .about meta-command +* DONE .version meta-command * TODO .license meta-command * TODO .help meta-command -* TODO parse insert statements in the form +* DONE parse insert statements in the form insert -** TODO Row struct -** TODO parse row insert -** TODO serialize/deserialize row to/from raw bytes -*** TODO look for best practices for creating binary formats +** DONE parse row insert +* DONE separate statements with semicolons +* DONE this error message could be better +#+begin example +Error: unexpected token + ╭─[ :1:24 ] + │ + 1 │ insert 0 "user" "email" + │ │ + │ ╰─ found end of file "" + │ + │ Note: expected token type to be one of ["semicolon"] +───╯ +#+end example +** plan +1. Create an example mapping system + - Define a mapping of token types to example values + - Example: "integer" → "42", "string" → "example", "semicolon" → ";" +2. Enhance CommandParseError + - Add a method to generate user-friendly error messages + - Include both the expected token type and concrete examples +3. Implementation approach + - Create a static lookup table or function that returns examples + - Extend existing error handling to include examples in messages + - Make sure the examples follow SQL syntax conventions +4. Error display refinement + - Update error_display.rs to include these examples + - Format error messages to show both what was expected and example syntax +5. Testing + - Add tests that verify the error messages include helpful examples + - Ensure examples are contextually appropriate + +This will make errors like "expected semicolon" more helpful by showing "expected semicolon (example: ;)". +* DONE correct all instances of in locations +* DONE meta-commands must be followed by end-of-file +* TODO project code documentation +* TODO project usage documentation +* DONE 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 * WAIT cli tests using insta-cmd https://insta.rs/docs/cmd/ diff --git a/src/branding.rs b/src/branding.rs index b4418d7..84e1e54 100644 --- a/src/branding.rs +++ b/src/branding.rs @@ -13,12 +13,18 @@ pub fn startup_msg() -> String { ) } -pub fn about_msg() -> String { +pub fn version_msg() -> String { let name = env!("CARGO_PKG_NAME"); let version = env!("CARGO_PKG_VERSION"); + format!("{name} v{version}") +} + +pub fn about_msg() -> String { + let version = version_msg(); + format!( - "{name} v{version} -- A database engine\n\ + "{version} -- A database engine\n\ Note: This is experimental software. No maintenance is intendend." ) } diff --git a/src/command.rs b/src/command.rs index 75c2792..10b1a97 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,6 +1,6 @@ use crate::meta_commands::{MetaCommand, MetaCommandExecuteResult}; use crate::statements::{Statement, StatementExecuteResult}; -use crate::tokens::{ScanError, Token}; +use crate::tokens::{Location, ScanError, Token}; #[derive(Debug)] pub enum Command { @@ -47,10 +47,48 @@ impl Command { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExpectedToken { + Integer, + String, + Semicolon, + Statement, + MetaCommand, + EndOfFile, +} + +impl ExpectedToken { + /// Returns an example value for this token type + pub fn example(&self) -> &'static str { + match self { + ExpectedToken::Integer => "42", + ExpectedToken::String => "\"example\"", + ExpectedToken::Semicolon => ";", + ExpectedToken::Statement => "select", + ExpectedToken::MetaCommand => ".exit", + ExpectedToken::EndOfFile => "", + } + } +} + +impl std::fmt::Display for ExpectedToken { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ExpectedToken::Integer => write!(f, "integer"), + ExpectedToken::String => write!(f, "string"), + ExpectedToken::Semicolon => write!(f, "semicolon"), + ExpectedToken::Statement => write!(f, "statement"), + ExpectedToken::MetaCommand => write!(f, "meta command"), + ExpectedToken::EndOfFile => write!(f, "end of file"), + } + } +} + #[derive(Debug)] pub enum CommandParseError { Scan(ScanError), - UnexpectedToken(Token, &'static [&'static str]), + UnexpectedToken(Token, &'static [ExpectedToken]), + UnexpectedEndOfFile(Location, &'static [ExpectedToken]), } impl From for Command { @@ -80,7 +118,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/error_display.rs b/src/error_display.rs index 45b237c..6466248 100644 --- a/src/error_display.rs +++ b/src/error_display.rs @@ -1,4 +1,7 @@ -use crate::{command::CommandParseError, tokens::ScanError}; +use crate::{ + command::{CommandParseError, ExpectedToken}, + tokens::ScanError, +}; use ariadne::{Color, Label, Report, ReportKind, Source}; pub trait OSDBError { @@ -11,24 +14,79 @@ impl OSDBError for CommandParseError { CommandParseError::Scan(x) => { x.display(file, input); } - CommandParseError::UnexpectedToken(token, items) => { + CommandParseError::UnexpectedToken(token, expected_tokens) => { let location = (file, Into::>::into(&token.location)); - Report::build(ReportKind::Error, location.clone()) + + let mut report = Report::build(ReportKind::Error, location.clone()) .with_message("unexpected token") .with_label( Label::new(location.clone()) .with_color(Color::Red) .with_message(format!("found {token}")), - ) - .with_note(format!("expected token type to be one of {items:?}")) - .finish() - .eprint((file, Source::from(input))) - .unwrap() + ); + + report = add_expected_tokens_to_report(report, expected_tokens); + + report.finish().eprint((file, Source::from(input))).unwrap() + } + CommandParseError::UnexpectedEndOfFile(location, expected_tokens) => { + let location = (file, Into::>::into(location)); + let report = Report::build(ReportKind::Error, location) + .with_message("unexpected end of file"); + let report = add_expected_tokens_to_report(report, expected_tokens); + report.finish().eprint((file, Source::from(input))).unwrap() } } } } +type OSDBReport<'a> = ariadne::ReportBuilder<'a, (&'a str, std::ops::Range)>; + +fn add_expected_tokens_to_report<'a>( + mut report: OSDBReport<'a>, + expected_tokens: &'static [ExpectedToken], +) -> OSDBReport<'a> { + // If we have expected tokens, show an example for the first one + if let Some(help) = expected_token_example_msg(expected_tokens) { + report = report.with_help(help) + } + + // If we have at least one expected token, show a message showing what type was expected + if let Some(note) = expected_token_msg(expected_tokens) { + report = report.with_note(note); + } + report +} + +fn expected_token_msg(expected_tokens: &'static [ExpectedToken]) -> Option { + if !expected_tokens.is_empty() { + // Add a note with all expected types + let expected_types: Vec<_> = expected_tokens.iter().map(|t| format!("{}", t)).collect(); + + // Use singular form when there's only one expected token type + Some(match expected_types.as_slice() { + [single_type] => { + format!("expected: {}", single_type) + } + _ => { + format!("expected one of: {}", expected_types.join(", ")) + } + }) + } else { + // there are no expected tokens + None + } +} + +fn expected_token_example_msg(expected_tokens: &'static [ExpectedToken]) -> Option { + if let Some(first_expected) = expected_tokens.first() { + let example = first_expected.example(); + Some(format!("try a token of the expected type: {example}")) + } else { + None + } +} + impl OSDBError for ScanError { fn display(&self, file: &str, input: &str) { let location = (file, Into::>::into(&self.location)); diff --git a/src/meta_commands.rs b/src/meta_commands.rs index d93a079..fb5d950 100644 --- a/src/meta_commands.rs +++ b/src/meta_commands.rs @@ -1,9 +1,10 @@ use crate::branding; -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, Clone)] pub enum MetaCommand { Exit, About, + Version, } impl std::fmt::Display for MetaCommand { @@ -11,6 +12,7 @@ impl std::fmt::Display for MetaCommand { match self { MetaCommand::Exit => write!(f, "exit"), MetaCommand::About => write!(f, "about"), + MetaCommand::Version => write!(f, "version"), } } } @@ -27,6 +29,10 @@ impl MetaCommand { print!("{}", branding::about_msg()); MetaCommandExecuteResult { should_exit: false } } + MetaCommand::Version => { + print!("{}", branding::version_msg()); + MetaCommandExecuteResult { should_exit: false } + } } } } diff --git a/src/parser.rs b/src/parser.rs index 7cff3b0..181f3e3 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,38 +1,241 @@ use std::collections::VecDeque; use crate::{ - command::{Command, CommandParseError}, + command::{Command, CommandParseError, ExpectedToken}, statements::Statement, - tokens::tokenize, + tokens::{Location, Token, TokenData, tokenize}, }; +// Helper function to skip tokens until reaching a semicolon or end of file +// This helps with error recovery when a statement has a syntax error +fn skip_to_next_statement(tokens: &mut VecDeque) { + while let Some(token) = tokens.front() { + match token.data { + TokenData::Semicolon | TokenData::EndOfFile => break, + _ => { + tokens.pop_front(); + } + } + } + + // Consume the semicolon if that's what we stopped at + if tokens + .front() + .is_some_and(|t| matches!(t.data, TokenData::Semicolon)) + { + tokens.pop_front(); + } +} + +// Helper function to check for a semicolon after a statement +fn expect_semicolon(tokens: &mut VecDeque) -> Result<(), CommandParseError> { + if let Some(next_token) = tokens.front() { + match next_token.data { + TokenData::Semicolon => { + tokens.pop_front(); // Consume the semicolon + Ok(()) + } + _ => Err(CommandParseError::UnexpectedToken( + next_token.clone(), + &[ExpectedToken::Semicolon], + )), + } + } else { + // Even at the end of input, we need a semicolon + Err(CommandParseError::UnexpectedToken( + Token { + location: tokens + .back() + .map_or_else(Location::default, |t| t.location.clone()), + data: TokenData::EndOfFile, + lexeme: String::new(), + }, + &[ExpectedToken::Semicolon], + )) + } +} + +fn parse_select_command( + tokens: &mut VecDeque, +) -> Result { + // Parse the select command (currently doesn't require additional tokens) + let cmd = Command::Statement(Statement::Select); + + // Check for semicolon after select command + expect_semicolon(tokens)?; + + Ok(cmd) +} + pub fn parse(file: String, input: String) -> Result, Vec> { let mut tokens: VecDeque<_> = tokenize(input, file) .map_err(|x| x.into_iter().map(|x| x.into()).collect::>())? .into(); let mut cmds = Vec::new(); let mut errs = Vec::new(); + while let Some(token) = tokens.pop_front() { match token.data { - crate::tokens::TokenData::Insert => cmds.push(Command::Statement(Statement::Insert)), - crate::tokens::TokenData::Select => cmds.push(Command::Statement(Statement::Select)), - crate::tokens::TokenData::MetaCommand(meta_command) => { - cmds.push(Command::MetaCommand(meta_command)) + TokenData::Insert => match parse_insert_command(&mut tokens) { + Ok(cmd) => cmds.push(cmd), + Err(err) => { + errs.push(err); + skip_to_next_statement(&mut tokens); // Skip to next statement for error recovery + } + }, + TokenData::Select => match parse_select_command(&mut tokens) { + Ok(cmd) => cmds.push(cmd), + Err(err) => { + errs.push(err); + skip_to_next_statement(&mut tokens); // Skip to next statement for error recovery + } + }, + TokenData::MetaCommand(meta_command) => { + match parse_meta_command(meta_command, &mut tokens) { + Ok(cmd) => cmds.push(cmd), + Err(err) => { + errs.push(err); + skip_to_next_statement(&mut tokens); // Skip to next statement for error recovery + } + } } - crate::tokens::TokenData::Int(_) => errs.push(CommandParseError::UnexpectedToken( - token, - &["statement", "meta command", "eof"], - )), - crate::tokens::TokenData::String(_) => errs.push(CommandParseError::UnexpectedToken( - token, - &["statement", "meta command", "eof"], - )), - crate::tokens::TokenData::EndOfFile => (), + TokenData::Semicolon => { + // Empty statement (just a semicolon) - ignore it + } + TokenData::Int(_) => { + errs.push(CommandParseError::UnexpectedToken( + token, + &[ + ExpectedToken::Statement, + ExpectedToken::MetaCommand, + ExpectedToken::EndOfFile, + ], + )); + skip_to_next_statement(&mut tokens); + } + TokenData::String(_) => { + errs.push(CommandParseError::UnexpectedToken( + token, + &[ + ExpectedToken::Statement, + ExpectedToken::MetaCommand, + ExpectedToken::EndOfFile, + ], + )); + skip_to_next_statement(&mut tokens); + } + TokenData::EndOfFile => (), // End of parsing } } + if errs.is_empty() { Ok(cmds) } else { Err(errs) } } +fn parse_meta_command( + meta_command: crate::meta_commands::MetaCommand, + tokens: &mut VecDeque, +) -> Result { + if let Some(token) = tokens.pop_front() { + if matches!(token.data, TokenData::EndOfFile) { + Ok(Command::MetaCommand(meta_command)) + } else { + Err(CommandParseError::UnexpectedToken( + token, + &[ExpectedToken::EndOfFile], + )) + } + } else { + Ok(Command::MetaCommand(meta_command)) + } +} + +fn parse_insert_command( + tokens: &mut VecDeque, +) -> Result { + // According to grammar.ebnf, insert command should be: insert int string string semicolon + + // Parse the id (integer) + let id_token = tokens.pop_front().ok_or_else(|| { + CommandParseError::UnexpectedToken( + Token { + location: tokens + .back() + .map_or_else(Location::default, |t| t.location.clone()), + data: TokenData::EndOfFile, + lexeme: String::new(), + }, + &[ExpectedToken::Integer], + ) + })?; + + let id = match id_token.data { + TokenData::Int(id) => id, + _ => { + return Err(CommandParseError::UnexpectedToken( + id_token, + &[ExpectedToken::Integer], + )); + } + }; + + // Parse the username (string) + let username_token = tokens.pop_front().ok_or_else(|| { + CommandParseError::UnexpectedToken( + Token { + location: tokens + .back() + .map_or_else(Location::default, |t| t.location.clone()), + data: TokenData::EndOfFile, + lexeme: String::new(), + }, + &[ExpectedToken::String], + ) + })?; + + let username = match username_token.data { + TokenData::String(username) => username, + _ => { + return Err(CommandParseError::UnexpectedToken( + username_token, + &[ExpectedToken::String], + )); + } + }; + + // Parse the email (string) + let email_token = tokens.pop_front().ok_or_else(|| { + CommandParseError::UnexpectedToken( + Token { + location: tokens + .back() + .map_or_else(Location::default, |t| t.location.clone()), + data: TokenData::EndOfFile, + lexeme: String::new(), + }, + &[ExpectedToken::String], + ) + })?; + + let email = match email_token.data { + TokenData::String(email) => email, + _ => { + return Err(CommandParseError::UnexpectedToken( + email_token, + &[ExpectedToken::String], + )); + } + }; + + // Check for semicolon after the insert command + expect_semicolon(tokens)?; + + Ok(Command::Statement(Statement::Insert { + id, + username, + email, + })) +} + #[cfg(test)] mod tests { use super::*; @@ -42,10 +245,32 @@ mod tests { fn test_parse_single_correct() { let file = String::from(""); 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;"))); + } + + #[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] + fn test_parse_missing_semicolon() { + let file = String::from(""); 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"))); + assert_debug_snapshot!(parse( + file.clone(), + String::from(r#"insert 1 "username" "email@example.com""#) + )); } #[test] @@ -60,10 +285,22 @@ mod tests { #[test] fn test_parse_multiple_correct() { + let file = String::from(""); + assert_debug_snapshot!(parse(file.clone(), String::from("select; select; select;"))); + } + + #[test] + fn test_meta_command_require_eof() { + let file = String::from(""); + assert_debug_snapshot!(parse(file.clone(), String::from(".exit select; select;"))); + } + + #[test] + fn test_parse_multiple_statements_with_insert() { let file = String::from(""); assert_debug_snapshot!(parse( file.clone(), - String::from(".exit select select insert select") + String::from(r#"select; insert 1 "user" "email@test.com"; 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__meta_command_require_eof.snap b/src/snapshots/osdb__parser__tests__meta_command_require_eof.snap new file mode 100644 index 0000000..fb9fffe --- /dev/null +++ b/src/snapshots/osdb__parser__tests__meta_command_require_eof.snap @@ -0,0 +1,22 @@ +--- +source: src/parser.rs +expression: "parse(file.clone(), String::from(\".exit select; select;\"))" +--- +Err( + [ + UnexpectedToken( + Token { + location: Location { + file: "", + offset: 6, + length: 6, + }, + data: Select, + lexeme: "select", + }, + [ + EndOfFile, + ], + ), + ], +) 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..a4a6547 --- /dev/null +++ b/src/snapshots/osdb__parser__tests__parse_insert_command-2.snap @@ -0,0 +1,24 @@ +--- +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, + ], + ), + ], +) 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..8a505fc --- /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: 1, + }, + data: Semicolon, + 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_missing_semicolon-2.snap b/src/snapshots/osdb__parser__tests__parse_missing_semicolon-2.snap new file mode 100644 index 0000000..9802ae3 --- /dev/null +++ b/src/snapshots/osdb__parser__tests__parse_missing_semicolon-2.snap @@ -0,0 +1,22 @@ +--- +source: src/parser.rs +expression: "parse(file.clone(),\nString::from(r#\"insert 1 \"username\" \"email@example.com\"\"#))" +--- +Err( + [ + UnexpectedToken( + Token { + location: Location { + file: "", + offset: 39, + length: 0, + }, + data: EndOfFile, + lexeme: "", + }, + [ + Semicolon, + ], + ), + ], +) diff --git a/src/snapshots/osdb__parser__tests__parse_missing_semicolon.snap b/src/snapshots/osdb__parser__tests__parse_missing_semicolon.snap new file mode 100644 index 0000000..92022f3 --- /dev/null +++ b/src/snapshots/osdb__parser__tests__parse_missing_semicolon.snap @@ -0,0 +1,22 @@ +--- +source: src/parser.rs +expression: "parse(file.clone(), String::from(\"select\"))" +--- +Err( + [ + UnexpectedToken( + Token { + location: Location { + file: "", + offset: 6, + length: 0, + }, + data: EndOfFile, + lexeme: "", + }, + [ + Semicolon, + ], + ), + ], +) diff --git a/src/snapshots/osdb__parser__tests__parse_multiple_correct.snap b/src/snapshots/osdb__parser__tests__parse_multiple_correct.snap index a90d0ae..e3f4512 100644 --- a/src/snapshots/osdb__parser__tests__parse_multiple_correct.snap +++ b/src/snapshots/osdb__parser__tests__parse_multiple_correct.snap @@ -1,21 +1,15 @@ --- source: src/parser.rs -expression: "parse(file.clone(), String::from(\".exit select select insert select\"))" +expression: "parse(file.clone(), String::from(\"select; select; select;\"))" --- Ok( [ - MetaCommand( - Exit, - ), Statement( Select, ), Statement( Select, ), - Statement( - Insert, - ), Statement( Select, ), diff --git a/src/snapshots/osdb__parser__tests__parse_multiple_statements_with_insert.snap b/src/snapshots/osdb__parser__tests__parse_multiple_statements_with_insert.snap new file mode 100644 index 0000000..0305714 --- /dev/null +++ b/src/snapshots/osdb__parser__tests__parse_multiple_statements_with_insert.snap @@ -0,0 +1,21 @@ +--- +source: src/parser.rs +expression: "parse(file.clone(),\nString::from(r#\"select; insert 1 \"user\" \"email@test.com\"; select;\"#))" +--- +Ok( + [ + Statement( + Select, + ), + Statement( + Insert { + id: 1, + username: "user", + email: "email@test.com", + }, + ), + Statement( + Select, + ), + ], +) diff --git a/src/statements.rs b/src/statements.rs index 5ba1103..cf5b3fa 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: format!("insert {id:?} {username:?} {email:?}"), }, Statement::Select => StatementExecuteResult { msg: String::from("select"), diff --git a/src/tokens.rs b/src/tokens.rs index 2c3464b..88610d2 100644 --- a/src/tokens.rs +++ b/src/tokens.rs @@ -1,6 +1,6 @@ use crate::meta_commands::MetaCommand; -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, Clone)] pub enum TokenData { Insert, Select, @@ -8,9 +8,10 @@ pub enum TokenData { EndOfFile, Int(i64), String(String), + Semicolon, } -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, Clone)] pub struct Location { /// file name pub file: String, @@ -29,6 +30,12 @@ impl From<&Location> for std::ops::Range { } } +impl Default for Location { + fn default() -> Self { + Self::new(String::from(""), 0, 0) + } +} + impl Location { /// ``` /// use osdb::tokens::Location; @@ -46,7 +53,7 @@ impl Location { } } -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, Clone)] pub struct Token { /// Where in the input was this token found? pub location: Location, @@ -65,6 +72,7 @@ impl std::fmt::Display for Token { TokenData::EndOfFile => write!(f, "end of file"), TokenData::Int(x) => write!(f, "integer {x}"), TokenData::String(x) => write!(f, "string {x:?}"), + TokenData::Semicolon => write!(f, "semicolon"), }?; let lexeme = &self.lexeme; write!(f, " {lexeme:?}") @@ -161,6 +169,7 @@ impl Tokenizer { match word.to_lowercase().as_str() { ".exit" => Some(TokenData::MetaCommand(MetaCommand::Exit)), ".about" => Some(TokenData::MetaCommand(MetaCommand::About)), + ".version" => Some(TokenData::MetaCommand(MetaCommand::Version)), _ => None, } } @@ -306,6 +315,15 @@ impl Tokenizer { } } + fn scan_semicolon(&mut self) -> Result { + self.advance(); + Ok(Token { + location: self.previous_location(1), + data: TokenData::Semicolon, + lexeme: String::from(";"), + }) + } + fn scan_token(&mut self) -> Result, ScanError> { loop { if let Some(c) = self.peek() { @@ -317,6 +335,8 @@ impl Tokenizer { return self.scan_integer().map(Some); } else if c == '"' { return self.scan_string().map(Some); + } else if c == ';' { + return self.scan_semicolon().map(Some); } else if c.is_whitespace() { self.advance(); } else {