diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 0fda3b6..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "rust-analyzer.linkedProjects": [ - "./squirrel-server/Cargo.toml", - "./squirrel-client/Cargo.toml", - "./squirrel-server/Cargo.toml", - "./squirrel-server/Cargo.toml" - ] -} \ No newline at end of file diff --git a/squirrel-client/src/main.rs b/squirrel-client/src/main.rs index c1f18fd..556ade9 100644 --- a/squirrel-client/src/main.rs +++ b/squirrel-client/src/main.rs @@ -1,7 +1,6 @@ -use std::net::{TcpStream}; -use std::io::{Read, Write}; -use std::str::from_utf8; use std::io; +use std::io::{Read, Write}; +use std::net::TcpStream; fn main() { match TcpStream::connect("localhost:5433") { @@ -17,17 +16,20 @@ fn main() { let msg = msg_str.as_bytes(); stream.write(msg).unwrap(); - - let mut response_size_buffer = [0 as u8; 8]; + + let mut response_size_buffer = [0 as u8; 8]; stream.read_exact(&mut response_size_buffer).unwrap(); let response_size: usize = usize::from_le_bytes(response_size_buffer); - let mut response_buffer = vec![0 as u8; response_size]; + let mut response_buffer = vec![0 as u8; response_size]; stream.read_exact(&mut response_buffer).unwrap(); - println!("{}", String::from_utf8(response_buffer).expect("a utf-8 string")); + println!( + "{}", + String::from_utf8(response_buffer).expect("a utf-8 string") + ); } - }, + } Err(e) => { println!("Failed to connect: {}", e); } } -} \ No newline at end of file +} diff --git a/squirrel-client/target/.rustc_info.json b/squirrel-client/target/.rustc_info.json index 5a4292b..2cd979f 100644 --- a/squirrel-client/target/.rustc_info.json +++ b/squirrel-client/target/.rustc_info.json @@ -1 +1 @@ -{"rustc_fingerprint":15117991565403657335,"outputs":{"10376369925670944939":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/nickorlow/.rustup/toolchains/stable-x86_64-apple-darwin\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_feature=\"sse3\"\ntarget_feature=\"ssse3\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""},"4614504638168534921":{"success":true,"status":"","code":0,"stdout":"rustc 1.66.1 (90743e729 2023-01-10)\nbinary: rustc\ncommit-hash: 90743e7298aca107ddaa0c202a4d3604e29bfeb6\ncommit-date: 2023-01-10\nhost: x86_64-apple-darwin\nrelease: 1.66.1\nLLVM version: 15.0.2\n","stderr":""},"15697416045686424142":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n","stderr":""}},"successes":{}} \ No newline at end of file +{"rustc_fingerprint":17949831956763062573,"outputs":{"15729799797837862367":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/nickorlow/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""},"4614504638168534921":{"success":true,"status":"","code":0,"stdout":"rustc 1.69.0 (84c898d65 2023-04-16)\nbinary: rustc\ncommit-hash: 84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc\ncommit-date: 2023-04-16\nhost: x86_64-unknown-linux-gnu\nrelease: 1.69.0\nLLVM version: 15.0.7\n","stderr":""}},"successes":{}} \ No newline at end of file diff --git a/squirrel-client/target/debug/squirrel-client b/squirrel-client/target/debug/squirrel-client index 5a6e825..0e1f050 100755 Binary files a/squirrel-client/target/debug/squirrel-client and b/squirrel-client/target/debug/squirrel-client differ diff --git a/squirrel-client/target/debug/squirrel-client.d b/squirrel-client/target/debug/squirrel-client.d index 218090e..7f9eba8 100644 --- a/squirrel-client/target/debug/squirrel-client.d +++ b/squirrel-client/target/debug/squirrel-client.d @@ -1 +1 @@ -/Users/nickorlow/programming/personal/SQUIRREL/squirrel-client/target/debug/squirrel-client: /Users/nickorlow/programming/personal/SQUIRREL/squirrel-client/src/main.rs +/home/nickorlow/programming/personal/squirrel/squirrel-client/target/debug/squirrel-client: /home/nickorlow/programming/personal/squirrel/squirrel-client/src/main.rs diff --git a/squirrel-server/Cargo.lock b/squirrel-server/Cargo.lock index 9ac9470..e12a1f1 100644 --- a/squirrel-server/Cargo.lock +++ b/squirrel-server/Cargo.lock @@ -5,3 +5,83 @@ version = 3 [[package]] name = "SQUIRREL" version = "0.1.0" +dependencies = [ + "anyhow", + "fancy-regex", + "regex", +] + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "regex" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" diff --git a/squirrel-server/Cargo.toml b/squirrel-server/Cargo.toml index 1ba3f23..35e50cd 100644 --- a/squirrel-server/Cargo.toml +++ b/squirrel-server/Cargo.toml @@ -6,3 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = "1.0.72" +fancy-regex = "0.11.0" +regex = "1.9.1" diff --git a/squirrel-server/data/tabledefs/users b/squirrel-server/data/tabledefs/users deleted file mode 100644 index a02dd8c..0000000 --- a/squirrel-server/data/tabledefs/users +++ /dev/null @@ -1,2 +0,0 @@ -id integer 0 -name varchar 24 diff --git a/squirrel-server/src/main.rs b/squirrel-server/src/main.rs index 392d51c..cf7732e 100644 --- a/squirrel-server/src/main.rs +++ b/squirrel-server/src/main.rs @@ -1,42 +1,125 @@ -use std::thread; -use std::net::{TcpListener, TcpStream, Shutdown}; -use std::io::{Read, Write}; -use core::str::Split; -use std::error::Error; +use std::collections::{HashMap, HashSet}; use std::fs; +use std::io::{BufRead, BufReader, Read, Write}; +use std::net::{Shutdown, TcpListener, TcpStream}; +use std::thread; mod parser; pub use parser::command::Command; mod table; -use parser::command::CreateCommand; +use parser::command::{CreateCommand, InsertCommand, SelectCommand}; pub use table::datatypes::Datatype; -pub use table::table::TableDefinition; +pub use table::table::{ColumnDefinition, TableDefinition}; + +use crate::parser::command::InsertItem; const BUFFER_SIZE: usize = 500; +fn handle_create(command: CreateCommand) -> ::anyhow::Result { + let mut file = fs::File::create(format!( + "./data/tabledefs/{}", + command.table_definition.name + ))?; -/* -CREATE TABLE [IF NOT EXISTS] table_name ( - column1 datatype(length) column_contraint, - column2 datatype(length) column_contraint, - column3 datatype(length) column_contraint, - table_constraints -); - */ -fn handle_create(command: CreateCommand) -> Result { - println!("Creating table with name: {}", command.table_definition.name); - let mut file = fs::File::create(format!("./data/tabledefs/{}", command.table_definition.name)).unwrap(); - for column in &command.table_definition.column_defs { - println!("creating col: {} {} {}", column.name, column.data_type.as_str(), column.length); - let line = format!("{} {} {} \n", column.name, column.data_type.as_str(), column.length); + let line = format!( + "{} {} {} \n", + column.name, + column.data_type.as_str(), + column.length + ); file.write_all(line.as_bytes()).unwrap(); } return Ok(command.table_definition); } +fn read_tabledef(table_name: String) -> ::anyhow::Result { + let file = fs::File::open(format!("./data/tabledefs/{}", table_name))?; + + let mut column_defs = vec![]; + + for line in BufReader::new(file).lines() { + let line_str = line?; + let parts: Vec<&str> = line_str.split(" ").collect(); + let col_def = ColumnDefinition { + name: parts[0].to_string(), + data_type: Datatype::from_str(parts[1]).unwrap(), + length: parts[2].parse::()?.into(), + }; + column_defs.push(col_def); + } + + return Ok(TableDefinition { + name: table_name, + column_defs, + }); +} + +fn handle_insert(command: InsertCommand) -> ::anyhow::Result<()> { + let mut file = fs::OpenOptions::new() + .create(true) + .write(true) + .append(true) + .open(format!("./data/blobs/{}", command.table_name)) + .unwrap(); + + let tabledef = read_tabledef(command.table_name).unwrap(); + + for col_def in &tabledef.column_defs { + if let Some(insert_item) = command.items.get(&col_def.name) { + let bytes = col_def + .data_type + .to_bytes(insert_item.column_value.clone())?; + file.write_all(&bytes)?; + if bytes.len() < col_def.length { + let length = col_def.length - bytes.len(); + let empty_bytes = vec![0; length]; + file.write_all(&empty_bytes)?; + } + } else { + return Err(anyhow::anyhow!( + "ERROR: INSERT statement is missing data for column '{}'", + col_def.name + )); + } + } + + return Ok(()); +} + +fn handle_select(command: SelectCommand) -> ::anyhow::Result { + let mut file = fs::File::open(format!("./data/blobs/{}", command.table_name))?; + let tabledef = read_tabledef(command.table_name).unwrap(); + let mut response = String::new(); + + response += "| "; + for col_def in &tabledef.column_defs { + response += format!("{} | ", col_def.name).as_str(); + } + response += "\n"; + response += "-----------\n"; + let mut buf: Vec = vec![0; tabledef.get_byte_size()]; + while file.read_exact(buf.as_mut_slice()).is_ok() { + response += "| "; + let mut idx = 0; + for col_def in &tabledef.column_defs { + let len = if col_def.length > 0 { + col_def.length + } else { + 1 + }; + let str_val = col_def.data_type.from_bytes(&buf[idx..(idx + len)])?; + response += format!("{} | ", str_val).as_str(); + idx += len; + } + response += "\n"; + } + + return Ok(response); +} + fn run_command(query: String) -> String { let response: String; if query.chars().nth(0).unwrap() == '\\' { @@ -44,60 +127,57 @@ fn run_command(query: String) -> String { return String::from("Slash commands are not yet supported in SQUIRREL"); } - let command_result: Result = Command::from_string(query); + let command_result: ::anyhow::Result = Command::from_string(query); if command_result.is_ok() { let command: Command = command_result.unwrap(); response = match command { - Command::Create(create_command) => { - let result_result = handle_create(create_command); + Command::Create(create_command) => { + let result_result = handle_create(create_command); if result_result.is_err() { - String::from("Error creating table.") + String::from("Error creating table.") } else { - String::from("Table created.") + String::from("Table created.") } } - _ => { String::from("Invalid command") } + Command::Insert(insert_command) => { + let result = handle_insert(insert_command); + if result.is_err() { + String::from(result.err().unwrap().to_string()) + } else { + String::from("Data inserted.") + } + } + Command::Select(select_command) => { + return handle_select(select_command).unwrap(); + } + _ => String::from("Invalid command"), } } else { - response = command_result.err().unwrap(); + response = command_result.err().unwrap().to_string(); } - + return response; } fn handle_client(mut stream: TcpStream) { - let mut data = [0 as u8; BUFFER_SIZE]; + let mut data = [0 as u8; BUFFER_SIZE]; while match stream.read(&mut data) { Ok(size) => { - let mut query_string = String::from_utf8(data.to_vec()).expect("A UTF-8 string"); - println!("Received: {}", query_string); + let query_string = String::from_utf8(data.to_vec()).expect("A UTF-8 string"); + let response: String = run_command(query_string); - let mut i = 0; - for c in query_string.chars() { - if c == ';' { - query_string = query_string.get(0..i).unwrap().to_string(); - i = 0; - break; - } - i += 1; - } - - let response: String; - if i == 0 { - response = run_command(query_string); - } else { - response = String::from("No semicolon."); - } - let mut response_data_size = response.len().to_le_bytes(); stream.write(&mut response_data_size).unwrap(); // send length of message stream.write(response.as_bytes()).unwrap(); // send message true - }, + } Err(_) => { - println!("An error occurred, terminating connection with {}", stream.peer_addr().unwrap()); + println!( + "An error occurred, terminating connection with {}", + stream.peer_addr().unwrap() + ); stream.shutdown(Shutdown::Both).unwrap(); false } @@ -105,10 +185,10 @@ fn handle_client(mut stream: TcpStream) { } fn main() -> std::io::Result<()> { - fs::remove_dir_all("./data")?; - fs::create_dir("./data")?; - fs::create_dir("./data/tabledefs")?; - fs::create_dir("./data/blobs")?; + //fs::remove_dir_all("./data")?; + let _ensure_data_exists = fs::create_dir("./data"); + let _ensure_tabledefs_exists = fs::create_dir("./data/tabledefs"); + let _ensure_blob_exists = fs::create_dir("./data/blobs"); let listener = TcpListener::bind("0.0.0.0:5433")?; for stream in listener.incoming() { @@ -119,4 +199,81 @@ fn main() -> std::io::Result<()> { } Ok(()) -} \ No newline at end of file +} + +#[test] +fn insert_statement() -> anyhow::Result<()> { + let empty_statement = ""; + let regular_statement = "INSERT INTO users (id, name) VALUES (1, \"Test\");"; + let extra_ws_statement = + "INSERT INTO users (id, name) VALUES (1, \"Test\") ;"; + let min_ws_statement = "INSERT INTO users(id, name) VALUES(1, \"Test\");"; + let str_comma_statement = "INSERT INTO users(id, name) VALUES(1, \"Firstname, Lastname\");"; + + let expected_output = Command::Insert(InsertCommand { + table_name: "users".to_string(), + items: HashMap::from([ + ( + "id".to_string(), + InsertItem { + column_name: "id".to_string(), + column_value: "1".to_string(), + }, + ), + ( + "name".to_string(), + InsertItem { + column_name: "name".to_string(), + column_value: "\"Test\"".to_string(), + }, + ), + ]), + }); + + let expected_output_comma = Command::Insert(InsertCommand { + table_name: "users".to_string(), + items: HashMap::from([ + ( + "id".to_string(), + InsertItem { + column_name: "id".to_string(), + column_value: "1".to_string(), + }, + ), + ( + "name".to_string(), + InsertItem { + column_name: "name".to_string(), + column_value: "\"Firstname, Lastname\"".to_string(), + }, + ), + ]), + }); + + assert_eq!( + Command::from_string(String::from(empty_statement)).is_ok(), + false + ); + + assert_eq!( + Command::from_string(String::from(regular_statement))?, + expected_output + ); + + assert_eq!( + Command::from_string(String::from(extra_ws_statement))?, + expected_output + ); + + assert_eq!( + Command::from_string(String::from(min_ws_statement))?, + expected_output + ); + + assert_eq!( + Command::from_string(String::from(str_comma_statement))?, + expected_output_comma + ); + + Ok(()) +} diff --git a/squirrel-server/src/parser/command.rs b/squirrel-server/src/parser/command.rs index 3fa8a64..7942bb0 100644 --- a/squirrel-server/src/parser/command.rs +++ b/squirrel-server/src/parser/command.rs @@ -1,65 +1,349 @@ -use crate::{TableDefinition, Datatype}; -use crate::table::table::Column; +use std::collections::{HashMap, HashSet}; +use crate::table::table::ColumnDefinition; +use crate::{Datatype, TableDefinition}; +use anyhow::anyhow; + +#[derive(Debug, Eq, PartialEq)] pub enum Command { - Select, + Select(SelectCommand), Create(CreateCommand), - Insert, - Delete + Insert(InsertCommand), + Delete, } +#[derive(Debug, Eq, PartialEq)] pub struct CreateCommand { pub table_definition: TableDefinition, } -impl Command { - pub fn from_string(command_str: String) -> Result { - let mut parts = command_str.split(' '); +#[derive(Debug, Eq, PartialEq)] +pub struct InsertCommand { + pub table_name: String, + pub items: HashMap, +} - match parts.nth(0).unwrap() { - "CREATE" => { - let object = String::from(parts.nth(0).unwrap()); - if object.eq_ignore_ascii_case("TABLE") { - let mut column_definitions: Vec = vec![]; +#[derive(Debug, Eq, PartialEq)] +pub struct SelectCommand { + pub table_name: String, + // TODO Later: pub column_names: Vec, +} - let column_def_begin_idx = command_str.chars().position(|c| c == '(').unwrap() + 1; - let column_def_end_idx = command_str.chars().position(|c| c == ')').unwrap(); - let coldef_str = command_str.get(column_def_begin_idx..column_def_end_idx).unwrap().to_string(); - let col_strs = coldef_str.split(','); +#[derive(Debug, Eq, PartialEq)] +pub struct InsertItem { + pub column_name: String, + pub column_value: String, +} - for col_str in col_strs { - println!("{}", col_str); - let mut parts = col_str.split_ascii_whitespace(); - let mut col: Column = Column { - length: 0, - name: parts.nth(0).unwrap().to_string(), - data_type: Datatype::from_str(parts.nth(0).unwrap()).unwrap() - }; - let len = parts.nth(0); - if len.is_some() { - if col.data_type.has_len() { - col.length = len.unwrap().parse().unwrap(); - } else { - return Err(format!("ERROR: Datatype '{}' does not accept a length parameter", col.data_type.as_str())); - } - } else if col.data_type.has_len() { - return Err(format!("ERROR: Datatype '{}' requires a length parameter", col.data_type.as_str())); - } - - column_definitions.push(col); - } +enum CreateParserState { + FindObject, + FindTableName, + FindColumnName, + FindColumnDefinitions, + FindColumnDatatype, + FindColumnDefinitionEnd, + FindColumnLength, + FindSemicolon, +} - return Ok(Command::Create(CreateCommand { - table_definition: TableDefinition { - name: String::from(parts.nth(0).unwrap()), - column_defs: column_definitions - } - })) - } else { - return Err(format!("ERROR: syntax error at or near '{}'", object)); - } - }, - _ => { Err(String::from("Unable to parse command")) } +enum SelectParserState { + FindWildcard, // Temporary, col selection coming soon + FindFrom, + FindTableName, + FindSemicolon, +} + +enum InsertParserState { + FindIntoKeyword, + FindTableName, + FindColumnListBegin, + FindColumnName, + FindColumnNameEnd, + FindValuesKeyword, + FindValuesListBegin, + FindValue, + FindValueEnd, + FindSemicolon, +} + +pub fn tokenizer(text: String) -> Vec { + let parts = HashSet::from([' ', ',', ';', '(', ')']); + let mut tokens: Vec = vec![]; + let mut cur_str = String::new(); + let mut in_quotes = false; + + for cur_char in text.chars() { + if cur_char == '\"' { + in_quotes = !in_quotes; + } + + if !in_quotes && parts.contains(&cur_char) { + if cur_str.len() != 0 { + tokens.push(cur_str); + cur_str = String::new(); + } + if cur_char != ' ' { + tokens.push(cur_char.to_string()); + } + } else { + cur_str.push(cur_char); } } -} \ No newline at end of file + + return tokens; +} + +impl Command { + fn parse_insert_command(tokens: &mut Vec) -> ::anyhow::Result { + let mut state: InsertParserState = InsertParserState::FindIntoKeyword; + + let mut table_name = String::new(); + let mut column_name = String::new(); + let mut column_val = String::new(); + + let mut column_list: Vec = vec![]; + let mut value_list: Vec = vec![]; + + while let Some(token) = &tokens.pop() { + match state { + InsertParserState::FindIntoKeyword => { + if !token.eq_ignore_ascii_case("INTO") { + return Err(anyhow!("Expected to find INTO at or near '{}'", token)); + } else { + state = InsertParserState::FindTableName; + } + } + InsertParserState::FindTableName => { + table_name = token.to_string(); + state = InsertParserState::FindColumnListBegin; + } + InsertParserState::FindColumnListBegin => { + if token != "(" { + return Err(anyhow!( + "Unexpected token at or near '{}'. Expected start of column list", + token + )); + } + state = InsertParserState::FindColumnName; + } + InsertParserState::FindColumnName => { + column_name = token.to_string(); + state = InsertParserState::FindColumnNameEnd; + } + InsertParserState::FindColumnNameEnd => { + if token == "," { + state = InsertParserState::FindColumnName; + } else if token == ")" { + state = InsertParserState::FindValuesKeyword; + } else { + return Err(anyhow!( + "Unexpected token at or near '{}'. Expected comma or rparen.", + token + )); + } + column_list.push(column_name.clone()); + } + InsertParserState::FindValuesKeyword => { + if token != "VALUES" { + return Err(anyhow!( + "Unexpected token at or near '{}'. Expected 'VALUES'.", + token + )); + } + state = InsertParserState::FindValuesListBegin; + } + InsertParserState::FindValuesListBegin => { + if token != "(" { + return Err(anyhow!( + "Unexpected token at or near '{}'. Expected start of values list", + token + )); + } + state = InsertParserState::FindValue; + } + InsertParserState::FindValue => { + column_val = token.to_string(); + state = InsertParserState::FindValueEnd; + } + InsertParserState::FindValueEnd => { + if token == "," { + state = InsertParserState::FindValue; + } else if token == ")" { + state = InsertParserState::FindSemicolon; + } else { + return Err(anyhow!( + "Unexpected token at or near '{}'. Expected comma or rparen.", + token + )); + } + + value_list.push(column_val.clone()); + } + InsertParserState::FindSemicolon => { + if token != ";" { + return Err(anyhow!("Expected semicolon at or near '{}'", token)); + } else { + let mut insert_item_list: HashMap = HashMap::new(); + for item in column_list.iter().zip(&mut value_list.iter_mut()) { + let (col_name, value) = item; + + insert_item_list.insert( + col_name.clone().trim().to_string(), + InsertItem { + column_name: col_name.trim().to_string(), + column_value: value.trim().to_string(), + }, + ); + } + return Ok(Command::Insert(InsertCommand { + table_name, + items: insert_item_list, + })); + } + } + } + } + + return Err(anyhow!("Unexpected end of input")); + } + + fn parse_select_command(tokens: &mut Vec) -> ::anyhow::Result { + let mut state: SelectParserState = SelectParserState::FindWildcard; + + // intermediate tmp vars + let mut table_name = String::new(); + + while let Some(token) = &tokens.pop() { + match state { + SelectParserState::FindWildcard => { + if token != "*" { + return Err(anyhow!("Expected to find selection at or near '{}' (SQUIRREL does not support column seletion)", token)); + } else { + state = SelectParserState::FindFrom; + } + } + SelectParserState::FindFrom => { + if !token.eq_ignore_ascii_case("FROM") { + return Err(anyhow!("Expected to find FROM at or near '{}'", token)); + } else { + state = SelectParserState::FindTableName; + } + } + SelectParserState::FindTableName => { + table_name = token.to_string(); + state = SelectParserState::FindSemicolon; + } + SelectParserState::FindSemicolon => { + if token != ";" { + return Err(anyhow!("Expected semicolon at or near '{}'", token)); + } else { + return Ok(Command::Select(SelectCommand { table_name })); + } + } + } + } + + return Err(anyhow!("Unexpected end of input")); + } + + fn parse_create_command(tokens: &mut Vec) -> ::anyhow::Result { + let mut state: CreateParserState = CreateParserState::FindObject; + let mut col_defs: Vec = vec![]; + + // intermediate tmp vars + let mut table_name = String::new(); + let mut data_type: Option = None; + let mut length = 0; + let mut col_name = String::new(); + + while let Some(token) = &tokens.pop() { + match state { + CreateParserState::FindObject => match token.to_uppercase().as_str() { + "TABLE" => { + state = CreateParserState::FindTableName; + } + _ => return Err(anyhow!("Can't create object of type '{}'", token.as_str())), + }, + CreateParserState::FindTableName => { + state = CreateParserState::FindColumnDefinitions; + table_name = token.to_string(); + } + CreateParserState::FindColumnDefinitions => { + if token != "(" { + return Err(anyhow!("Could not find column list")); + } else { + state = CreateParserState::FindColumnName; + } + } + CreateParserState::FindColumnName => { + col_name = token.to_string(); + state = CreateParserState::FindColumnDatatype; + } + CreateParserState::FindColumnDatatype => { + let dtype = Datatype::from_str(&token).unwrap(); + if dtype.has_len() { + state = CreateParserState::FindColumnLength; + } else { + state = CreateParserState::FindColumnDefinitionEnd; + } + data_type = Some(dtype); + } + CreateParserState::FindColumnLength => { + length = token.parse()?; + state = CreateParserState::FindColumnDefinitionEnd; + } + CreateParserState::FindColumnDefinitionEnd => { + let column_def = ColumnDefinition { + data_type: data_type.unwrap(), + length, + name: col_name, + }; + + length = 0; + col_name = String::new(); + data_type = None; + + col_defs.push(column_def); + + match token.as_str() { + "," => { + state = CreateParserState::FindColumnName; + } + ")" => { + state = CreateParserState::FindSemicolon; + } + _ => return Err(anyhow!("Expected end")), + } + } + CreateParserState::FindSemicolon => { + if token != ";" { + return Err(anyhow!("Expected semicolon at or near '{}'", token)); + } else { + return Ok(Command::Create(CreateCommand { + table_definition: TableDefinition { + name: table_name, + column_defs: col_defs, + }, + })); + } + } + } + } + + return Err(anyhow!("Unexpected end of input")); + } + + pub fn from_string(command_str: String) -> ::anyhow::Result { + let mut tokens: Vec = tokenizer(command_str); + tokens.reverse(); + if let Some(token) = tokens.pop() { + return match token.to_uppercase().as_str() { + "CREATE" => Self::parse_create_command(&mut tokens), + "INSERT" => Self::parse_insert_command(&mut tokens), + "SELECT" => Self::parse_select_command(&mut tokens), + _ => Err(anyhow!("Unknown command '{}'", token)), + }; + } + return Err(anyhow!("Unexpected end of statement")); + } +} diff --git a/squirrel-server/src/parser/mod.rs b/squirrel-server/src/parser/mod.rs index ab8b7ed..9fe7961 100644 --- a/squirrel-server/src/parser/mod.rs +++ b/squirrel-server/src/parser/mod.rs @@ -1 +1 @@ -pub mod command; \ No newline at end of file +pub mod command; diff --git a/squirrel-server/src/table/datatypes.rs b/squirrel-server/src/table/datatypes.rs index 5c435de..22e8f6a 100644 --- a/squirrel-server/src/table/datatypes.rs +++ b/squirrel-server/src/table/datatypes.rs @@ -1,3 +1,4 @@ +#[derive(Debug, Eq, PartialEq)] pub enum Datatype { Integer, CharacterVarying, @@ -7,25 +8,60 @@ impl Datatype { pub fn as_str(&self) -> &'static str { match self { Datatype::CharacterVarying => "varchar", - Datatype::Integer => "integer" + Datatype::Integer => "integer", } } pub fn has_len(&self) -> bool { match self { Datatype::CharacterVarying => true, - Datatype::Integer => false + Datatype::Integer => false, } } + pub fn to_bytes(&self, data_val: String) -> ::anyhow::Result> { + match self { + Datatype::CharacterVarying => { + // Ensure string is formatted properly + if !data_val.starts_with('\"') || !data_val.ends_with('\"') { + return Err(::anyhow::anyhow!( + "ERROR: Unable to parse value for type CharacterVarying" + )); + } + let mut str_bytes = data_val.as_bytes().to_vec(); + + // Remove dquotes + str_bytes.remove(0); + str_bytes.remove(str_bytes.len() - 1); + return Ok(str_bytes); + } + Datatype::Integer => { + let val = data_val.parse::()?; + return Ok(vec![val]); + } + } + } + + pub fn from_bytes(&self, data_val: &[u8]) -> ::anyhow::Result { + match self { + Datatype::CharacterVarying => { + let str_val = String::from_utf8(data_val.to_vec())?; + return Ok(str_val); + } + Datatype::Integer => { + let val = data_val.first().unwrap(); + return Ok(format!("{}", val)); + } + } + } pub fn from_str(string: &str) -> Result { match string { "varchar" => return Ok(Datatype::CharacterVarying), "character varying" => return Ok(Datatype::CharacterVarying), "integer" => return Ok(Datatype::Integer), "int" => return Ok(Datatype::Integer), - "int4" => return Ok(Datatype::Integer), - _ => {return Err(String::from("Undefined data type"))} + "int8" => return Ok(Datatype::Integer), + _ => return Err(String::from("Undefined data type")), } } -} \ No newline at end of file +} diff --git a/squirrel-server/src/table/table.rs b/squirrel-server/src/table/table.rs index 3e3d20b..6786d17 100644 --- a/squirrel-server/src/table/table.rs +++ b/squirrel-server/src/table/table.rs @@ -1,12 +1,31 @@ use crate::Datatype; -pub struct Column { +#[derive(Debug, Eq, PartialEq)] +pub struct ColumnDefinition { pub name: String, pub data_type: Datatype, - pub length: u16 // used for char(n), varchar(n) + pub length: usize, // used for char(n), varchar(n) } +#[derive(Debug, Eq, PartialEq)] pub struct TableDefinition { pub name: String, - pub column_defs: Vec, -} \ No newline at end of file + pub column_defs: Vec, +} + +impl TableDefinition { + pub fn get_byte_size(&self) -> usize { + let mut sum: usize = 0; + for col_def in self.column_defs.iter() { + // TODO HACK FIXME + // We should keep track of length + // even for built-in datatypes. + sum += if col_def.length > 0 { + col_def.length + } else { + 1 + }; + } + return sum; + } +}