feat(parser): implement insert command parsing

Implements the parse_insert_command function to handle the insert
statement according to the grammar definition. The function now
correctly parses the command format insert int string string and creates
a structured Statement::Insert with the id, username, and email fields.
This commit is contained in:
Khaïs COLIN 2025-05-31 16:08:42 +02:00
parent 315703d46b
commit 28cb288eaf
Signed by: logistic-bot
SSH key fingerprint: SHA256:RlpiqKeXpcPFZZ4y9Ou4xi2M8OhRJovIwDlbCaMsuAo
12 changed files with 228 additions and 20 deletions

View file

@ -1,3 +1,4 @@
/* token is first stage of parsing */
token ::= insert token ::= insert
| select | select
| meta-command | meta-command
@ -5,6 +6,12 @@ token ::= insert
| string | string
| end-of-file | 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" insert ::= "insert"
select ::= "select" select ::= "select"

View file

@ -235,6 +235,8 @@ i will use rustyline, since it seems like the most feature-complete
insert <id:int> <username:string> <email:string> insert <id:int> <username:string> <email:string>
** TODO Row struct ** TODO Row struct
** TODO parse row insert ** 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 serialize/deserialize row to/from raw bytes
*** TODO look for best practices for creating binary formats *** TODO look for best practices for creating binary formats

View file

@ -80,7 +80,12 @@ mod tests {
#[test] #[test]
fn test_execute_insert_statement() { 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(); let result = statement.execute().display();
assert_snapshot!(result); assert_snapshot!(result);
} }

View file

@ -32,7 +32,7 @@ impl MetaCommand {
MetaCommand::Version => { MetaCommand::Version => {
print!("{}", branding::version_msg()); print!("{}", branding::version_msg());
MetaCommandExecuteResult { should_exit: false } MetaCommandExecuteResult { should_exit: false }
}, }
} }
} }
} }

View file

@ -3,7 +3,7 @@ use std::collections::VecDeque;
use crate::{ use crate::{
command::{Command, CommandParseError}, command::{Command, CommandParseError},
statements::Statement, statements::Statement,
tokens::tokenize, tokens::{Location, Token, TokenData, tokenize},
}; };
pub fn parse(file: String, input: String) -> Result<Vec<Command>, Vec<CommandParseError>> { pub fn parse(file: String, input: String) -> Result<Vec<Command>, Vec<CommandParseError>> {
@ -14,7 +14,10 @@ pub fn parse(file: String, input: String) -> Result<Vec<Command>, Vec<CommandPar
let mut errs = Vec::new(); let mut errs = Vec::new();
while let Some(token) = tokens.pop_front() { while let Some(token) = tokens.pop_front() {
match token.data { match token.data {
crate::tokens::TokenData::Insert => 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::Select => cmds.push(Command::Statement(Statement::Select)),
crate::tokens::TokenData::MetaCommand(meta_command) => { crate::tokens::TokenData::MetaCommand(meta_command) => {
cmds.push(Command::MetaCommand(meta_command)) cmds.push(Command::MetaCommand(meta_command))
@ -33,6 +36,83 @@ pub fn parse(file: String, input: String) -> Result<Vec<Command>, Vec<CommandPar
if errs.is_empty() { Ok(cmds) } else { Err(errs) } if errs.is_empty() { Ok(cmds) } else { Err(errs) }
} }
fn parse_insert_command(
tokens: &mut VecDeque<crate::tokens::Token>,
) -> Result<Command, CommandParseError> {
// 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("<unknown>"), 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("<unknown>"), 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("<unknown>"), 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; 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(".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("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("<stdin>");
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] #[test]
@ -63,7 +155,7 @@ mod tests {
let file = String::from("<stdin>"); let file = String::from("<stdin>");
assert_debug_snapshot!(parse( assert_debug_snapshot!(parse(
file.clone(), file.clone(),
String::from(".exit select select insert select") String::from(".exit select select select")
)); ));
} }

View file

@ -2,4 +2,4 @@
source: src/command.rs source: src/command.rs
expression: result expression: result
--- ---
insert insert 45 "user" "user@example.org"

View file

@ -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: "<stdin>",
offset: 7,
length: 11,
},
data: String(
"not_an_id",
),
lexeme: "\"not_an_id\"",
},
[
"integer",
],
),
UnexpectedToken(
Token {
location: Location {
file: "<stdin>",
offset: 19,
length: 10,
},
data: String(
"username",
),
lexeme: "\"username\"",
},
[
"statement",
"meta command",
"eof",
],
),
UnexpectedToken(
Token {
location: Location {
file: "<stdin>",
offset: 30,
length: 19,
},
data: String(
"email@example.com",
),
lexeme: "\"email@example.com\"",
},
[
"statement",
"meta command",
"eof",
],
),
],
)

View file

@ -0,0 +1,22 @@
---
source: src/parser.rs
expression: "parse(file.clone(), String::from(r#\"insert 1 \"username\"\"#))"
---
Err(
[
UnexpectedToken(
Token {
location: Location {
file: "<stdin>",
offset: 19,
length: 0,
},
data: EndOfFile,
lexeme: "",
},
[
"string",
],
),
],
)

View file

@ -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",
},
),
],
)

View file

@ -1,6 +1,6 @@
--- ---
source: src/parser.rs 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( Ok(
[ [
@ -13,9 +13,6 @@ Ok(
Statement( Statement(
Select, Select,
), ),
Statement(
Insert,
),
Statement( Statement(
Select, Select,
), ),

View file

@ -1,6 +1,10 @@
#[derive(Debug)] #[derive(Debug)]
pub enum Statement { pub enum Statement {
Insert, Insert {
id: i64,
username: String,
email: String,
},
Select, Select,
} }
@ -11,8 +15,12 @@ pub struct StatementExecuteResult {
impl Statement { impl Statement {
pub fn execute(&self) -> StatementExecuteResult { pub fn execute(&self) -> StatementExecuteResult {
match self { match self {
Statement::Insert => StatementExecuteResult { Statement::Insert {
msg: String::from("insert"), id,
username,
email,
} => StatementExecuteResult {
msg: String::from(format!("insert {id:?} {username:?} {email:?}")),
}, },
Statement::Select => StatementExecuteResult { Statement::Select => StatementExecuteResult {
msg: String::from("select"), msg: String::from("select"),

View file

@ -10,7 +10,7 @@ pub enum TokenData {
String(String), String(String),
} }
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq, Clone)]
pub struct Location { pub struct Location {
/// file name /// file name
pub file: String, pub file: String,