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
| 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"

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>
** 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

View file

@ -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);
}

View file

@ -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 }
}
}
}
}

View file

@ -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<Command>, Vec<CommandParseError>> {
@ -14,7 +14,10 @@ pub fn parse(file: String, input: String) -> Result<Vec<Command>, Vec<CommandPar
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::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<Command>, Vec<CommandPar
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)]
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("<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]
@ -63,7 +155,7 @@ mod tests {
let file = String::from("<stdin>");
assert_debug_snapshot!(parse(
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
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
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,
),

View file

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

View file

@ -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,