From d959b9756c272d2a7098df3900d67de123e73fe7 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 29 Aug 2024 16:16:36 -0300 Subject: [PATCH 001/103] initial commit --- .gitignore | 1 + Cargo.toml | 6 ++++++ src/main.rs | 3 +++ 3 files changed, 10 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e9804cf --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "rustic-sql" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} From bf01139c2722ec719c787f0e0ce378c95893ef95 Mon Sep 17 00:00:00 2001 From: katta <102127372+gabokatta@users.noreply.github.com> Date: Thu, 29 Aug 2024 16:21:13 -0300 Subject: [PATCH 002/103] Create README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..73be8f0 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# rustic-sql 🦀 + +> woo! From 053169585141c882ecae51371adc30cb77603fd7 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 29 Aug 2024 16:36:22 -0300 Subject: [PATCH 003/103] initialized test folders --- .gitignore | 1 + Cargo.lock | 7 +++++++ tests/integration.rs | 0 3 files changed, 8 insertions(+) create mode 100644 Cargo.lock create mode 100644 tests/integration.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..3a8cabc 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.idea diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1de2011 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "rustic-sql" +version = "0.1.0" diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 0000000..e69de29 From 5eb7246ea2a3a462d7705afdf3c95e78d770f635 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 29 Aug 2024 16:41:22 -0300 Subject: [PATCH 004/103] test auth commit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 73be8f0..07f7fd2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # rustic-sql 🦀 -> woo! +> wooo! From 61e9b14b7fb44f1ce12ca427087bc01169ac2558 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Thu, 29 Aug 2024 16:43:13 -0300 Subject: [PATCH 005/103] test auth commit 2 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 07f7fd2..e985180 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # rustic-sql 🦀 -> wooo! +> woooo! From 86b1e2d90ce0627cc4ded9dcd1d98864ff40a62c Mon Sep 17 00:00:00 2001 From: gabokatta Date: Fri, 30 Aug 2024 22:13:16 -0300 Subject: [PATCH 006/103] added validation and began working on errors --- Cargo.lock | 9 +++++++++ Cargo.toml | 1 + src/errors.rs | 19 +++++++++++++++++++ src/files/errors.rs | 39 +++++++++++++++++++++++++++++++++++++++ src/files/mod.rs | 22 ++++++++++++++++++++++ src/main.rs | 23 +++++++++++++++++++++-- src/query/errors.rs | 35 +++++++++++++++++++++++++++++++++++ src/query/executor/mod.rs | 1 + src/query/mod.rs | 3 +++ src/query/parser/mod.rs | 1 + tests/integration.rs | 1 + 11 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 src/errors.rs create mode 100644 src/files/errors.rs create mode 100644 src/files/mod.rs create mode 100644 src/query/errors.rs create mode 100644 src/query/executor/mod.rs create mode 100644 src/query/mod.rs create mode 100644 src/query/parser/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 1de2011..24da147 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + [[package]] name = "rustic-sql" version = "0.1.0" +dependencies = [ + "log", +] diff --git a/Cargo.toml b/Cargo.toml index e9804cf..59b688b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,4 @@ version = "0.1.0" edition = "2021" [dependencies] +log = "0.4.22" diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..d3da0a4 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,19 @@ +use std::error::Error; +use std::fmt::{Debug, Display, Formatter}; + +/// Generic Error for the RusticSQL Application. +pub struct Errored(Box); + +impl Error for Errored {} + +impl Debug for Errored { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self) + } +} + +impl Display for Errored { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "ERROR: {}", self.0) + } +} diff --git a/src/files/errors.rs b/src/files/errors.rs new file mode 100644 index 0000000..2ff467d --- /dev/null +++ b/src/files/errors.rs @@ -0,0 +1,39 @@ +use std::error::Error; +use std::fmt::{Debug, Display, Formatter}; +use std::io; +pub enum FileError { + InvalidPath(String), + InvalidDirectory(String), + EmptyDirectory(String), + IOError(io::Error), +} + +impl Error for FileError {} +impl Debug for FileError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self) + } +} + +impl Display for FileError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + FileError::InvalidDirectory(path) => { + write!( + f, + "FileError: Path {} points to an invalid directory.", + path + ) + } + FileError::EmptyDirectory(path) => { + write!(f, "FileError: Path {} points to an empty directory.", path) + } + FileError::InvalidPath(path) => { + write!(f, "FileError: Path {} does not exist.", path) + } + FileError::IOError(e) => { + write!(f, "IOError: {}", e) + } + } + } +} diff --git a/src/files/mod.rs b/src/files/mod.rs new file mode 100644 index 0000000..31399ab --- /dev/null +++ b/src/files/mod.rs @@ -0,0 +1,22 @@ +use errors::FileError; +use std::path::Path; +use FileError::{EmptyDirectory, IOError, InvalidDirectory, InvalidPath}; + +mod errors; + +pub fn validate_path(dir: &str) -> Result<(), FileError> { + let path = Path::new(dir); + + if !path.exists() { + return Err(InvalidPath(dir.to_string())); + } + if !path.is_dir() { + return Err(InvalidDirectory(dir.to_string())); + } + + let dir_entries = path.read_dir().map_err(IOError)?; + if dir_entries.count() == 0 { + return Err(EmptyDirectory(dir.to_string())); + } + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..551b698 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,22 @@ -fn main() { - println!("Hello, world!"); +use files::validate_path; +use std::env; +use std::error::Error; +mod errors; +mod files; +mod query; + +fn main() -> Result<(), Box> { + let args: Vec = env::args().collect(); + if args.len() < 3 { + eprintln!("invalid usage of rustic-sql"); + return Err("usage: cargo run -- ".into()); + } + + let path: &String = &args[1]; + let query: &String = &args[2]; + + validate_path(path)?; + dbg!(path, query); + + Ok(()) } diff --git a/src/query/errors.rs b/src/query/errors.rs new file mode 100644 index 0000000..0107146 --- /dev/null +++ b/src/query/errors.rs @@ -0,0 +1,35 @@ +use std::error::Error; +use std::fmt::{Debug, Display, Formatter}; + +pub enum SQLError { + InvalidSyntax(String), + InvalidColumn(String), + InvalidTable(String), + Error(Box), +} + +impl Error for SQLError {} +impl Debug for SQLError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self) + } +} + +impl Display for SQLError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + SQLError::InvalidSyntax(syntax) => { + write!(f, "INVALID_SYNTAX: {}", syntax) + } + SQLError::InvalidColumn(column) => { + write!(f, "INVALID_COLUMN: {}", column) + } + SQLError::InvalidTable(table) => { + write!(f, "INVALID_TABLE: {}", table) + } + SQLError::Error(err) => { + write!(f, "ERROR: {}", err) + } + } + } +} diff --git a/src/query/executor/mod.rs b/src/query/executor/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/query/executor/mod.rs @@ -0,0 +1 @@ + diff --git a/src/query/mod.rs b/src/query/mod.rs new file mode 100644 index 0000000..9347863 --- /dev/null +++ b/src/query/mod.rs @@ -0,0 +1,3 @@ +mod errors; +mod executor; +mod parser; diff --git a/src/query/parser/mod.rs b/src/query/parser/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/query/parser/mod.rs @@ -0,0 +1 @@ + diff --git a/tests/integration.rs b/tests/integration.rs index e69de29..8b13789 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -0,0 +1 @@ + From 23ee444c93e2f27d92f9bb55accd0a7650b10fe8 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Fri, 30 Aug 2024 22:59:17 -0300 Subject: [PATCH 007/103] reworked errors --- src/errors.rs | 4 ++-- src/files/errors.rs | 39 --------------------------------------- src/files/mod.rs | 17 ++++++++--------- src/main.rs | 13 ++++++++++++- src/query/errors.rs | 38 +++++++++++++++++++++----------------- src/query/mod.rs | 9 +++++++++ 6 files changed, 52 insertions(+), 68 deletions(-) delete mode 100644 src/files/errors.rs diff --git a/src/errors.rs b/src/errors.rs index d3da0a4..0e60fbe 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -2,7 +2,7 @@ use std::error::Error; use std::fmt::{Debug, Display, Formatter}; /// Generic Error for the RusticSQL Application. -pub struct Errored(Box); +pub struct Errored(pub String); impl Error for Errored {} @@ -14,6 +14,6 @@ impl Debug for Errored { impl Display for Errored { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "ERROR: {}", self.0) + write!(f, "[ERROR]: {}", self.0) } } diff --git a/src/files/errors.rs b/src/files/errors.rs deleted file mode 100644 index 2ff467d..0000000 --- a/src/files/errors.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::error::Error; -use std::fmt::{Debug, Display, Formatter}; -use std::io; -pub enum FileError { - InvalidPath(String), - InvalidDirectory(String), - EmptyDirectory(String), - IOError(io::Error), -} - -impl Error for FileError {} -impl Debug for FileError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self) - } -} - -impl Display for FileError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - FileError::InvalidDirectory(path) => { - write!( - f, - "FileError: Path {} points to an invalid directory.", - path - ) - } - FileError::EmptyDirectory(path) => { - write!(f, "FileError: Path {} points to an empty directory.", path) - } - FileError::InvalidPath(path) => { - write!(f, "FileError: Path {} does not exist.", path) - } - FileError::IOError(e) => { - write!(f, "IOError: {}", e) - } - } - } -} diff --git a/src/files/mod.rs b/src/files/mod.rs index 31399ab..f6c5c93 100644 --- a/src/files/mod.rs +++ b/src/files/mod.rs @@ -1,22 +1,21 @@ -use errors::FileError; +use crate::errors::Errored; use std::path::Path; -use FileError::{EmptyDirectory, IOError, InvalidDirectory, InvalidPath}; -mod errors; - -pub fn validate_path(dir: &str) -> Result<(), FileError> { +pub fn validate_path(dir: &str) -> Result<(), Errored> { let path = Path::new(dir); if !path.exists() { - return Err(InvalidPath(dir.to_string())); + return Err(Errored(format!("path {} does not exist", dir))); } if !path.is_dir() { - return Err(InvalidDirectory(dir.to_string())); + return Err(Errored(format!("path {} is not a valid directory", dir))); } - let dir_entries = path.read_dir().map_err(IOError)?; + let dir_entries = path + .read_dir() + .map_err(|e| Errored(format!("failure when reading directory {}: {}", dir, e)))?; if dir_entries.count() == 0 { - return Err(EmptyDirectory(dir.to_string())); + return Err(Errored(format!("path {} is an empty directory", dir))); } Ok(()) } diff --git a/src/main.rs b/src/main.rs index 551b698..2c81390 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,22 @@ +use crate::query::validate_query_string; use files::validate_path; use std::env; use std::error::Error; + mod errors; mod files; mod query; fn main() -> Result<(), Box> { - let args: Vec = env::args().collect(); + + if let Err(e) = run(env::args().collect()) { + eprintln!("{}", e); + } + Ok(()) +} + +fn run(args: Vec) -> Result<(), Box> { + if args.len() < 3 { eprintln!("invalid usage of rustic-sql"); return Err("usage: cargo run -- ".into()); @@ -16,6 +26,7 @@ fn main() -> Result<(), Box> { let query: &String = &args[2]; validate_path(path)?; + validate_query_string(query)?; dbg!(path, query); Ok(()) diff --git a/src/query/errors.rs b/src/query/errors.rs index 0107146..22c0bd4 100644 --- a/src/query/errors.rs +++ b/src/query/errors.rs @@ -1,34 +1,38 @@ use std::error::Error; use std::fmt::{Debug, Display, Formatter}; -pub enum SQLError { - InvalidSyntax(String), - InvalidColumn(String), - InvalidTable(String), - Error(Box), +pub enum InvalidSQL { + Syntax(String), + Column(String, String), + Table(String, String), } -impl Error for SQLError {} -impl Debug for SQLError { +impl Error for InvalidSQL {} +impl Debug for InvalidSQL { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self) } } -impl Display for SQLError { +impl Display for InvalidSQL { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - SQLError::InvalidSyntax(syntax) => { - write!(f, "INVALID_SYNTAX: {}", syntax) + InvalidSQL::Syntax(syntax) => { + write!(f, "[INVALID_SYNTAX]: {}", syntax) } - SQLError::InvalidColumn(column) => { - write!(f, "INVALID_COLUMN: {}", column) + InvalidSQL::Column(column, details) => { + write!( + f, + "[INVALID_COLUMN]: column {} is invalid because {}.", + column, details + ) } - SQLError::InvalidTable(table) => { - write!(f, "INVALID_TABLE: {}", table) - } - SQLError::Error(err) => { - write!(f, "ERROR: {}", err) + InvalidSQL::Table(table, details) => { + write!( + f, + "[INVALID_TABLE]: table {} is invalid because {}.", + table, details + ) } } } diff --git a/src/query/mod.rs b/src/query/mod.rs index 9347863..fa1d54c 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -1,3 +1,12 @@ mod errors; mod executor; mod parser; + +use crate::errors::Errored; + +pub fn validate_query_string(query: &str) -> Result<(), Errored> { + if query.trim().is_empty() { + return Err(Errored(String::from("query is empty."))); + } + Ok(()) +} From 5fbfdfcb05f360f4a5525ecffc3be4178bbd22ec Mon Sep 17 00:00:00 2001 From: gabokatta Date: Fri, 30 Aug 2024 22:59:31 -0300 Subject: [PATCH 008/103] format --- src/main.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2c81390..1c3f868 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,15 +8,13 @@ mod files; mod query; fn main() -> Result<(), Box> { - if let Err(e) = run(env::args().collect()) { eprintln!("{}", e); } Ok(()) } -fn run(args: Vec) -> Result<(), Box> { - +fn run(args: Vec) -> Result<(), Box> { if args.len() < 3 { eprintln!("invalid usage of rustic-sql"); return Err("usage: cargo run -- ".into()); From ce94067aa8c71e602ce06d73c37eae0e5cf5d99a Mon Sep 17 00:00:00 2001 From: gabokatta Date: Fri, 30 Aug 2024 23:03:24 -0300 Subject: [PATCH 009/103] format --- src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 1c3f868..ae28564 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,8 @@ mod files; mod query; fn main() -> Result<(), Box> { - if let Err(e) = run(env::args().collect()) { + let args = env::args().collect(); + if let Err(e) = run(args) { eprintln!("{}", e); } Ok(()) From 78d6f22e842bb5fa9125b996ec66330149b3156d Mon Sep 17 00:00:00 2001 From: gabokatta Date: Fri, 30 Aug 2024 23:05:30 -0300 Subject: [PATCH 010/103] made errors output to stdout instead of stderr per tp requirements --- src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index ae28564..f79e303 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ -use crate::query::validate_query_string; use files::validate_path; +use query::validate_query_string; use std::env; use std::error::Error; @@ -10,14 +10,14 @@ mod query; fn main() -> Result<(), Box> { let args = env::args().collect(); if let Err(e) = run(args) { - eprintln!("{}", e); + println!("{}", e); } Ok(()) } fn run(args: Vec) -> Result<(), Box> { if args.len() < 3 { - eprintln!("invalid usage of rustic-sql"); + println!("invalid usage of rustic-sql"); return Err("usage: cargo run -- ".into()); } From 03ec858e7df81794bad36791a424a644788946f8 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Fri, 30 Aug 2024 23:31:18 -0300 Subject: [PATCH 011/103] slight refactor to string formatting in errored --- src/files/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/files/mod.rs b/src/files/mod.rs index f6c5c93..d66b834 100644 --- a/src/files/mod.rs +++ b/src/files/mod.rs @@ -5,17 +5,17 @@ pub fn validate_path(dir: &str) -> Result<(), Errored> { let path = Path::new(dir); if !path.exists() { - return Err(Errored(format!("path {} does not exist", dir))); + return Err(Errored(format!("path '{dir}' does not exist"))); } if !path.is_dir() { - return Err(Errored(format!("path {} is not a valid directory", dir))); + return Err(Errored(format!("path '{dir}' is not a valid directory"))); } let dir_entries = path .read_dir() - .map_err(|e| Errored(format!("failure when reading directory {}: {}", dir, e)))?; + .map_err(|e| Errored(format!("failure when reading directory '{dir}': {e}")))?; if dir_entries.count() == 0 { - return Err(Errored(format!("path {} is an empty directory", dir))); + return Err(Errored(format!("path '{dir}' is an empty directory"))); } Ok(()) } From cc6a99c0fa5636d3da9d1d9909c95f44da530e31 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 1 Sep 2024 00:22:06 -0300 Subject: [PATCH 012/103] added base for finite state machine (really simple implementation) --- src/query/executor/mod.rs | 1 - src/query/mod.rs | 103 +++++++++++++++++++++++++++++++++++++- src/query/parser/mod.rs | 1 - 3 files changed, 101 insertions(+), 4 deletions(-) delete mode 100644 src/query/executor/mod.rs delete mode 100644 src/query/parser/mod.rs diff --git a/src/query/executor/mod.rs b/src/query/executor/mod.rs deleted file mode 100644 index 8b13789..0000000 --- a/src/query/executor/mod.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/query/mod.rs b/src/query/mod.rs index fa1d54c..41e4783 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -1,8 +1,107 @@ mod errors; -mod executor; -mod parser; use crate::errors::Errored; +use crate::query::errors::InvalidSQL; +use crate::query::ParserState::{IdentifierOrKeyword, New, NumberLiteral, Operator, StringLiteral}; + +pub struct Parser { + index: usize, + state: ParserState, +} + +pub struct Query { + pub kind: String, + pub table: String, + pub fields: Vec, + pub conditions: Vec, +} + +impl Query { + pub fn new() -> Self { + Self { + kind: String::new(), + table: String::new(), + fields: vec![], + conditions: vec![], + } + } +} + +impl Parser { + pub fn new() -> Self { + Self { + index: 0, + state: New, + } + } + + pub fn parse(&mut self, sql: String) -> Result { + let tokens = self.tokenize(sql)?; + Ok(Query::new()) + } + + pub fn tokenize(&mut self, sql: String) -> Result, InvalidSQL> { + let tokens = vec![]; + while self.index < sql.len() { + if let Some(char) = self.curr_char(&sql) { + match &self.state { + New => { + if char.is_whitespace() { + self.index += 1 + } else if char.is_alphabetic() { + self.state = IdentifierOrKeyword + } else if char.is_digit(10) { + self.state = NumberLiteral + } else if char == '\'' { + self.state = StringLiteral; + } else { + self.state = Operator + } + } + IdentifierOrKeyword => self.tokenize_identifier_or_keyword(), + NumberLiteral => self.tokenize_number_literal(), + StringLiteral => self.tokenize_string_literal(), + Operator => self.tokenize_operator(), + } + } else { + break; + } + } + Ok(tokens) + } + + fn tokenize_string_literal(&self) {} + + fn tokenize_number_literal(&self) {} + + fn tokenize_operator(&self) {} + + fn tokenize_identifier_or_keyword(&self) {} + + fn curr_char(&self, sql: &str) -> Option { + sql[self.index..].chars().next() + } +} + +enum ParserState { + New, + IdentifierOrKeyword, + NumberLiteral, + StringLiteral, + Operator, +} + +pub struct Token { + value: String, + kind: TokenKind, +} + +enum TokenKind { + Keyword, + Literal, + Identifier, + Operator, +} pub fn validate_query_string(query: &str) -> Result<(), Errored> { if query.trim().is_empty() { diff --git a/src/query/parser/mod.rs b/src/query/parser/mod.rs deleted file mode 100644 index 8b13789..0000000 --- a/src/query/parser/mod.rs +++ /dev/null @@ -1 +0,0 @@ - From 4ecfd499b66a4f0eb6859d11dae15555791b66b5 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 1 Sep 2024 00:24:30 -0300 Subject: [PATCH 013/103] slight clippy fix --- src/query/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/query/mod.rs b/src/query/mod.rs index 41e4783..b154fc0 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -50,7 +50,7 @@ impl Parser { self.index += 1 } else if char.is_alphabetic() { self.state = IdentifierOrKeyword - } else if char.is_digit(10) { + } else if char.is_ascii_digit() { self.state = NumberLiteral } else if char == '\'' { self.state = StringLiteral; From a1994d54e82dca9e5fad5943194765fca0a8b245 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 1 Sep 2024 05:28:09 -0300 Subject: [PATCH 014/103] rollbacked parser approach, will try another route --- src/main.rs | 7 ++- src/query/mod.rs | 129 ++++++++++++++++++++--------------------------- 2 files changed, 61 insertions(+), 75 deletions(-) diff --git a/src/main.rs b/src/main.rs index f79e303..bcf0d94 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use crate::query::{Parser, Tokenizer}; use files::validate_path; use query::validate_query_string; use std::env; @@ -26,7 +27,11 @@ fn run(args: Vec) -> Result<(), Box> { validate_path(path)?; validate_query_string(query)?; - dbg!(path, query); + let mut tokenizer = Tokenizer::new(); + let tokens = tokenizer.tokenize(query)?; + for t in tokens { + println!("{:?}", t) + } Ok(()) } diff --git a/src/query/mod.rs b/src/query/mod.rs index b154fc0..a0f72d9 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -2,105 +2,86 @@ mod errors; use crate::errors::Errored; use crate::query::errors::InvalidSQL; -use crate::query::ParserState::{IdentifierOrKeyword, New, NumberLiteral, Operator, StringLiteral}; +use crate::query::TokenizerState::Begin; -pub struct Parser { +pub struct Tokenizer { index: usize, - state: ParserState, + state: TokenizerState, } -pub struct Query { - pub kind: String, - pub table: String, - pub fields: Vec, - pub conditions: Vec, +#[derive(Debug)] +pub struct Query {} + +#[derive(Debug)] +pub struct Token { + pub value: String, + kind: TokenKind, } -impl Query { - pub fn new() -> Self { - Self { - kind: String::new(), - table: String::new(), - fields: vec![], - conditions: vec![], - } - } +enum TokenizerState { + Begin, +} + +#[derive(Debug)] +enum TokenKind { + Literal, + Operator, + Identifier, + Keyword, } -impl Parser { +impl Tokenizer { pub fn new() -> Self { Self { index: 0, - state: New, + state: Begin, } } - pub fn parse(&mut self, sql: String) -> Result { - let tokens = self.tokenize(sql)?; - Ok(Query::new()) + pub fn tokenize(&mut self, sql: &String) -> Result, InvalidSQL> { + Ok(vec![]) } - pub fn tokenize(&mut self, sql: String) -> Result, InvalidSQL> { - let tokens = vec![]; - while self.index < sql.len() { - if let Some(char) = self.curr_char(&sql) { - match &self.state { - New => { - if char.is_whitespace() { - self.index += 1 - } else if char.is_alphabetic() { - self.state = IdentifierOrKeyword - } else if char.is_ascii_digit() { - self.state = NumberLiteral - } else if char == '\'' { - self.state = StringLiteral; - } else { - self.state = Operator - } - } - IdentifierOrKeyword => self.tokenize_identifier_or_keyword(), - NumberLiteral => self.tokenize_number_literal(), - StringLiteral => self.tokenize_string_literal(), - Operator => self.tokenize_operator(), - } - } else { - break; - } - } - Ok(tokens) + fn char_at(&self, index: usize, sql: &str) -> char { + sql[index..].chars().next().unwrap_or('\0') } - fn tokenize_string_literal(&self) {} - - fn tokenize_number_literal(&self) {} - - fn tokenize_operator(&self) {} - - fn tokenize_identifier_or_keyword(&self) {} - - fn curr_char(&self, sql: &str) -> Option { - sql[self.index..].chars().next() + fn reset(&mut self) { + self.state = Begin } } -enum ParserState { - New, - IdentifierOrKeyword, - NumberLiteral, - StringLiteral, - Operator, +fn valid_operators() -> Vec { + vec![ + "=".to_string(), + "<".to_string(), + ">".to_string(), + ">=".to_string(), + "<=".to_string(), + "!=".to_string(), + "<>".to_string(), + ] } -pub struct Token { - value: String, - kind: TokenKind, +fn skippable_chars() -> Vec { + vec![' ', '(', ')', ',', ';'] } -enum TokenKind { - Keyword, - Literal, - Identifier, - Operator, +fn reserved_keywords() -> Vec { + vec![ + "SELECT".to_string(), + "UPDATE".to_string(), + "DELETE".to_string(), + "INSERT INTO".to_string(), + "VALUES".to_string(), + "ORDER BY".to_string(), + "DESC".to_string(), + "ASC".to_string(), + "FROM".to_string(), + "WHERE".to_string(), + "AND".to_string(), + "OR".to_string(), + ] } pub fn validate_query_string(query: &str) -> Result<(), Errored> { From 238dcbddcbdb16e3130810129b2263d18e679cd2 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 1 Sep 2024 20:43:08 -0300 Subject: [PATCH 015/103] tokenizer is a work in progress, weird bug with multibyte chars --- src/main.rs | 4 +- src/query/mod.rs | 200 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 187 insertions(+), 17 deletions(-) diff --git a/src/main.rs b/src/main.rs index bcf0d94..12980f2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use crate::query::{Parser, Tokenizer}; +use crate::query::Tokenizer; use files::validate_path; use query::validate_query_string; use std::env; @@ -17,7 +17,7 @@ fn main() -> Result<(), Box> { } fn run(args: Vec) -> Result<(), Box> { - if args.len() < 3 { + if args.len() != 3 { println!("invalid usage of rustic-sql"); return Err("usage: cargo run -- ".into()); } diff --git a/src/query/mod.rs b/src/query/mod.rs index a0f72d9..053c751 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -2,10 +2,14 @@ mod errors; use crate::errors::Errored; use crate::query::errors::InvalidSQL; -use crate::query::TokenizerState::Begin; +use crate::query::errors::InvalidSQL::Syntax; +use crate::query::TokenKind::{Identifier, Keyword, Number, Unknown}; +use crate::query::TokenizerState::{ + Begin, Complete, IdentifierOrKeyword, NumberLiteral, Operator, StringLiteral, +}; pub struct Tokenizer { - index: usize, + i: usize, state: TokenizerState, } @@ -15,16 +19,32 @@ pub struct Query {} #[derive(Debug)] pub struct Token { pub value: String, - kind: TokenKind, + pub kind: TokenKind, +} + +impl Token { + pub fn new() -> Self { + Self { + value: String::new(), + kind: Unknown, + } + } } enum TokenizerState { Begin, + IdentifierOrKeyword, + Operator, + NumberLiteral, + StringLiteral, + Complete, } -#[derive(Debug)] -enum TokenKind { - Literal, +#[derive(Debug, PartialEq)] +pub enum TokenKind { + Unknown, + String, + Number, Operator, Identifier, Keyword, @@ -32,18 +52,149 @@ enum TokenKind { impl Tokenizer { pub fn new() -> Self { - Self { - index: 0, - state: Begin, + Self { i: 0, state: Begin } + } + + pub fn tokenize(&mut self, sql: &str) -> Result, InvalidSQL> { + let mut out = vec![]; + let mut token = Token::new(); + while self.i < sql.len() { + let c = char_at(self.i, sql); + match self.state { + Begin => self.next_state(c)?, + IdentifierOrKeyword => token = self.tokenize_identifier_or_keyword(sql)?, + Operator => token = self.tokenize_operator(sql)?, + NumberLiteral => token = self.tokenize_number(sql)?, + StringLiteral => { + self.i += 1; + token = self.tokenize_quoted(sql)?; + } + Complete => { + out.push(token); + token = Token::new(); + self.reset() + } + } + } + if token.kind != Unknown { + out.push(token); + } + Ok(out) + } + + fn next_state(&mut self, c: char) -> Result<(), InvalidSQL> { + match c { + c if can_be_skipped(c) => self.i += 1, + c if c.is_ascii_digit() => self.state = NumberLiteral, + c if is_identifier_char(c) => self.state = IdentifierOrKeyword, + '\'' => self.state = StringLiteral, + c if is_operator_char(c) => self.state = Operator, + _ => { + return Err(Syntax(format!( + "could not tokenize char: {} at index: {}.", + c, self.i + ))) + } } + Ok(()) } - pub fn tokenize(&mut self, sql: &String) -> Result, InvalidSQL> { - Ok(vec![]) + fn tokenize_identifier_or_keyword(&mut self, sql: &str) -> Result { + let start = self.i; + if let Some(word) = self.matches_keyword(sql) { + self.i += word.len(); + self.state = Complete; + return Ok(Token { + value: word, + kind: Keyword, + }); + } + while self.i < sql.len() && is_identifier_char(char_at(self.i, sql)) { + self.i += 1; + } + let identifier = &sql[start..self.i]; + self.state = Complete; + Ok(Token { + value: String::from(identifier), + kind: Identifier, + }) } - fn char_at(&self, index: usize, sql: &str) -> char { - sql[index..].chars().next().unwrap_or('\0') + fn tokenize_operator(&mut self, sql: &str) -> Result { + if let Some(op) = self.matches_operator(sql) { + self.i += op.len(); + self.state = Complete; + Ok(Token { + value: op, + kind: TokenKind::Operator, + }) + } else { + Err(Syntax(format!( + "unrecognized operator {} at index: {}", + char_at(self.i, sql), + self.i + ))) + } + } + + fn matches_keyword(&self, sql: &str) -> Option { + for word in reserved_keywords() { + let end = self.i + word.len(); + if end <= sql.len() { + let token = &sql[self.i..end]; + let next_char = char_at(end, sql); + if token.to_uppercase() == word.as_str() && !is_identifier_char(next_char) { + return Some(word); + } + } + } + None + } + + fn matches_operator(&self, sql: &str) -> Option { + for op in valid_operators() { + let end = self.i + op.len(); + if end <= sql.len() { + let token = &sql[self.i..end]; + if token == op.as_str() && !is_operator_char(char_at(end, sql)) { + return Some(op); + } + } + } + None + } + + fn tokenize_number(&mut self, sql: &str) -> Result { + let start = self.i; + while self.i < sql.len() && char_at(self.i, sql).is_ascii_digit() { + self.i += 1; + } + let number = &sql[start..self.i]; + self.state = Complete; + Ok(Token { + value: String::from(number), + kind: Number, + }) + } + + fn tokenize_quoted(&mut self, sql: &str) -> Result { + let start = self.i; + for (index, char) in sql[start..].char_indices() { + if char == '\'' { + self.i = start + index + 1; + let quoted = &sql[start..start + index]; + self.state = Complete; + return Ok(Token { + value: String::from(quoted), + kind: TokenKind::String, + }); + } + } + + Err(Syntax(format!( + "unclosed quotation mark after index: {}", + start + ))) } fn reset(&mut self) { @@ -51,8 +202,27 @@ impl Tokenizer { } } +fn char_at(index: usize, string: &str) -> char { + let mut chars = string.chars(); + let at_index = chars.nth(index); + at_index.unwrap_or('\0') +} + +fn can_be_skipped(c: char) -> bool { + c.is_whitespace() || ignorable_chars().contains(&c) +} + +fn is_identifier_char(c: char) -> bool { + c == '_' || c.is_alphanumeric() && !can_be_skipped(c) +} + +fn is_operator_char(c: char) -> bool { + valid_operators().contains(&c.to_string()) +} + fn valid_operators() -> Vec { vec![ + "*".to_string(), "=".to_string(), "<".to_string(), ">".to_string(), @@ -63,8 +233,8 @@ fn valid_operators() -> Vec { ] } -fn skippable_chars() -> Vec { - vec![' ', '(', ')', ',', ';'] +fn ignorable_chars() -> Vec { + vec![' ', '(', ')', ',', ';', '\0', '\n'] } fn reserved_keywords() -> Vec { From b0ea9fba4f7890138937f00bdc9b945d971d1a82 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 1 Sep 2024 21:50:27 -0300 Subject: [PATCH 016/103] made tokenizer internal count be in bytes to handle accented chars. --- src/query/mod.rs | 84 ++++++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/src/query/mod.rs b/src/query/mod.rs index 053c751..aa054a9 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -13,9 +13,6 @@ pub struct Tokenizer { state: TokenizerState, } -#[derive(Debug)] -pub struct Query {} - #[derive(Debug)] pub struct Token { pub value: String, @@ -50,6 +47,9 @@ pub enum TokenKind { Keyword, } +#[derive(Debug)] +pub struct Query {} + impl Tokenizer { pub fn new() -> Self { Self { i: 0, state: Begin } @@ -66,7 +66,7 @@ impl Tokenizer { Operator => token = self.tokenize_operator(sql)?, NumberLiteral => token = self.tokenize_number(sql)?, StringLiteral => { - self.i += 1; + self.i += c.len_utf8(); token = self.tokenize_quoted(sql)?; } Complete => { @@ -84,7 +84,7 @@ impl Tokenizer { fn next_state(&mut self, c: char) -> Result<(), InvalidSQL> { match c { - c if can_be_skipped(c) => self.i += 1, + c if can_be_skipped(c) => self.i += c.len_utf8(), c if c.is_ascii_digit() => self.state = NumberLiteral, c if is_identifier_char(c) => self.state = IdentifierOrKeyword, '\'' => self.state = StringLiteral, @@ -109,9 +109,13 @@ impl Tokenizer { kind: Keyword, }); } - while self.i < sql.len() && is_identifier_char(char_at(self.i, sql)) { - self.i += 1; + + let mut c = char_at(self.i, sql); + while self.i < sql.len() && is_identifier_char(c) { + self.i += c.len_utf8(); + c = char_at(self.i, sql); } + let identifier = &sql[start..self.i]; self.state = Complete; Ok(Token { @@ -137,38 +141,15 @@ impl Tokenizer { } } - fn matches_keyword(&self, sql: &str) -> Option { - for word in reserved_keywords() { - let end = self.i + word.len(); - if end <= sql.len() { - let token = &sql[self.i..end]; - let next_char = char_at(end, sql); - if token.to_uppercase() == word.as_str() && !is_identifier_char(next_char) { - return Some(word); - } - } - } - None - } - - fn matches_operator(&self, sql: &str) -> Option { - for op in valid_operators() { - let end = self.i + op.len(); - if end <= sql.len() { - let token = &sql[self.i..end]; - if token == op.as_str() && !is_operator_char(char_at(end, sql)) { - return Some(op); - } - } - } - None - } - fn tokenize_number(&mut self, sql: &str) -> Result { let start = self.i; - while self.i < sql.len() && char_at(self.i, sql).is_ascii_digit() { - self.i += 1; + + let mut c = char_at(self.i, sql); + while self.i < sql.len() && c.is_ascii_digit() { + self.i += c.len_utf8(); + c = char_at(self.i, sql); } + let number = &sql[start..self.i]; self.state = Complete; Ok(Token { @@ -197,15 +178,40 @@ impl Tokenizer { ))) } + fn matches_keyword(&self, sql: &str) -> Option { + for word in reserved_keywords() { + let end = self.i + word.len(); + if end <= sql.len() { + let token = &sql[self.i..end]; + let next_char = char_at(end, sql); + if token.to_uppercase() == word.as_str() && !is_identifier_char(next_char) { + return Some(word); + } + } + } + None + } + + fn matches_operator(&self, sql: &str) -> Option { + for op in valid_operators() { + let end = self.i + op.len(); + if end <= sql.len() { + let token = &sql[self.i..end]; + if token == op.as_str() && !is_operator_char(char_at(end, sql)) { + return Some(op); + } + } + } + None + } + fn reset(&mut self) { self.state = Begin } } fn char_at(index: usize, string: &str) -> char { - let mut chars = string.chars(); - let at_index = chars.nth(index); - at_index.unwrap_or('\0') + string[index..].chars().next().unwrap_or('\0') } fn can_be_skipped(c: char) -> bool { From f73742f46ef5a4c69982027054aa6b2b32b244c4 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 1 Sep 2024 22:22:57 -0300 Subject: [PATCH 017/103] new modules --- src/main.rs | 2 +- src/query/builder.rs | 1 + src/query/executor.rs | 1 + src/query/mod.rs | 184 +---------------------------------------- src/query/tokenizer.rs | 182 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 189 insertions(+), 181 deletions(-) create mode 100644 src/query/builder.rs create mode 100644 src/query/executor.rs create mode 100644 src/query/tokenizer.rs diff --git a/src/main.rs b/src/main.rs index 12980f2..d1ca5a7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use crate::query::Tokenizer; +use crate::query::tokenizer::Tokenizer; use files::validate_path; use query::validate_query_string; use std::env; diff --git a/src/query/builder.rs b/src/query/builder.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/query/builder.rs @@ -0,0 +1 @@ + diff --git a/src/query/executor.rs b/src/query/executor.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/query/executor.rs @@ -0,0 +1 @@ + diff --git a/src/query/mod.rs b/src/query/mod.rs index aa054a9..5797953 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -1,17 +1,10 @@ +mod builder; mod errors; +mod executor; +pub mod tokenizer; use crate::errors::Errored; -use crate::query::errors::InvalidSQL; -use crate::query::errors::InvalidSQL::Syntax; -use crate::query::TokenKind::{Identifier, Keyword, Number, Unknown}; -use crate::query::TokenizerState::{ - Begin, Complete, IdentifierOrKeyword, NumberLiteral, Operator, StringLiteral, -}; - -pub struct Tokenizer { - i: usize, - state: TokenizerState, -} +use crate::query::TokenKind::Unknown; #[derive(Debug)] pub struct Token { @@ -28,15 +21,6 @@ impl Token { } } -enum TokenizerState { - Begin, - IdentifierOrKeyword, - Operator, - NumberLiteral, - StringLiteral, - Complete, -} - #[derive(Debug, PartialEq)] pub enum TokenKind { Unknown, @@ -50,166 +34,6 @@ pub enum TokenKind { #[derive(Debug)] pub struct Query {} -impl Tokenizer { - pub fn new() -> Self { - Self { i: 0, state: Begin } - } - - pub fn tokenize(&mut self, sql: &str) -> Result, InvalidSQL> { - let mut out = vec![]; - let mut token = Token::new(); - while self.i < sql.len() { - let c = char_at(self.i, sql); - match self.state { - Begin => self.next_state(c)?, - IdentifierOrKeyword => token = self.tokenize_identifier_or_keyword(sql)?, - Operator => token = self.tokenize_operator(sql)?, - NumberLiteral => token = self.tokenize_number(sql)?, - StringLiteral => { - self.i += c.len_utf8(); - token = self.tokenize_quoted(sql)?; - } - Complete => { - out.push(token); - token = Token::new(); - self.reset() - } - } - } - if token.kind != Unknown { - out.push(token); - } - Ok(out) - } - - fn next_state(&mut self, c: char) -> Result<(), InvalidSQL> { - match c { - c if can_be_skipped(c) => self.i += c.len_utf8(), - c if c.is_ascii_digit() => self.state = NumberLiteral, - c if is_identifier_char(c) => self.state = IdentifierOrKeyword, - '\'' => self.state = StringLiteral, - c if is_operator_char(c) => self.state = Operator, - _ => { - return Err(Syntax(format!( - "could not tokenize char: {} at index: {}.", - c, self.i - ))) - } - } - Ok(()) - } - - fn tokenize_identifier_or_keyword(&mut self, sql: &str) -> Result { - let start = self.i; - if let Some(word) = self.matches_keyword(sql) { - self.i += word.len(); - self.state = Complete; - return Ok(Token { - value: word, - kind: Keyword, - }); - } - - let mut c = char_at(self.i, sql); - while self.i < sql.len() && is_identifier_char(c) { - self.i += c.len_utf8(); - c = char_at(self.i, sql); - } - - let identifier = &sql[start..self.i]; - self.state = Complete; - Ok(Token { - value: String::from(identifier), - kind: Identifier, - }) - } - - fn tokenize_operator(&mut self, sql: &str) -> Result { - if let Some(op) = self.matches_operator(sql) { - self.i += op.len(); - self.state = Complete; - Ok(Token { - value: op, - kind: TokenKind::Operator, - }) - } else { - Err(Syntax(format!( - "unrecognized operator {} at index: {}", - char_at(self.i, sql), - self.i - ))) - } - } - - fn tokenize_number(&mut self, sql: &str) -> Result { - let start = self.i; - - let mut c = char_at(self.i, sql); - while self.i < sql.len() && c.is_ascii_digit() { - self.i += c.len_utf8(); - c = char_at(self.i, sql); - } - - let number = &sql[start..self.i]; - self.state = Complete; - Ok(Token { - value: String::from(number), - kind: Number, - }) - } - - fn tokenize_quoted(&mut self, sql: &str) -> Result { - let start = self.i; - for (index, char) in sql[start..].char_indices() { - if char == '\'' { - self.i = start + index + 1; - let quoted = &sql[start..start + index]; - self.state = Complete; - return Ok(Token { - value: String::from(quoted), - kind: TokenKind::String, - }); - } - } - - Err(Syntax(format!( - "unclosed quotation mark after index: {}", - start - ))) - } - - fn matches_keyword(&self, sql: &str) -> Option { - for word in reserved_keywords() { - let end = self.i + word.len(); - if end <= sql.len() { - let token = &sql[self.i..end]; - let next_char = char_at(end, sql); - if token.to_uppercase() == word.as_str() && !is_identifier_char(next_char) { - return Some(word); - } - } - } - None - } - - fn matches_operator(&self, sql: &str) -> Option { - for op in valid_operators() { - let end = self.i + op.len(); - if end <= sql.len() { - let token = &sql[self.i..end]; - if token == op.as_str() && !is_operator_char(char_at(end, sql)) { - return Some(op); - } - } - } - None - } - - fn reset(&mut self) { - self.state = Begin - } -} - fn char_at(index: usize, string: &str) -> char { string[index..].chars().next().unwrap_or('\0') } diff --git a/src/query/tokenizer.rs b/src/query/tokenizer.rs new file mode 100644 index 0000000..2538ec8 --- /dev/null +++ b/src/query/tokenizer.rs @@ -0,0 +1,182 @@ +use crate::query::errors::InvalidSQL; +use crate::query::errors::InvalidSQL::Syntax; +use crate::query::tokenizer::TokenizerState::*; +use crate::query::TokenKind::{Identifier, Keyword, Number, Unknown}; +use crate::query::{ + can_be_skipped, char_at, is_identifier_char, is_operator_char, reserved_keywords, + valid_operators, Token, TokenKind, +}; + +pub struct Tokenizer { + i: usize, + state: TokenizerState, +} + +enum TokenizerState { + Begin, + IdentifierOrKeyword, + Operator, + NumberLiteral, + StringLiteral, + Complete, +} + +impl Tokenizer { + pub fn new() -> Self { + Self { i: 0, state: Begin } + } + + pub fn tokenize(&mut self, sql: &str) -> Result, InvalidSQL> { + let mut out = vec![]; + let mut token = Token::new(); + while self.i < sql.len() { + let c = char_at(self.i, sql); + match self.state { + Begin => self.next_state(c)?, + IdentifierOrKeyword => token = self.tokenize_identifier_or_keyword(sql)?, + Operator => token = self.tokenize_operator(sql)?, + NumberLiteral => token = self.tokenize_number(sql)?, + StringLiteral => { + self.i += c.len_utf8(); + token = self.tokenize_quoted(sql)?; + } + Complete => { + out.push(token); + token = Token::new(); + self.reset() + } + } + } + if token.kind != Unknown { + out.push(token); + } + Ok(out) + } + + fn next_state(&mut self, c: char) -> Result<(), InvalidSQL> { + match c { + c if can_be_skipped(c) => self.i += c.len_utf8(), + c if c.is_ascii_digit() => self.state = NumberLiteral, + c if is_identifier_char(c) => self.state = IdentifierOrKeyword, + '\'' => self.state = StringLiteral, + c if is_operator_char(c) => self.state = Operator, + _ => { + return Err(Syntax(format!( + "could not tokenize char: {} at index: {}.", + c, self.i + ))) + } + } + Ok(()) + } + + fn tokenize_identifier_or_keyword(&mut self, sql: &str) -> Result { + let start = self.i; + if let Some(word) = self.matches_keyword(sql) { + self.i += word.len(); + self.state = Complete; + return Ok(Token { + value: word, + kind: Keyword, + }); + } + + let mut c = char_at(self.i, sql); + while self.i < sql.len() && is_identifier_char(c) { + self.i += c.len_utf8(); + c = char_at(self.i, sql); + } + + let identifier = &sql[start..self.i]; + self.state = Complete; + Ok(Token { + value: String::from(identifier), + kind: Identifier, + }) + } + + fn tokenize_operator(&mut self, sql: &str) -> Result { + if let Some(op) = self.matches_operator(sql) { + self.i += op.len(); + self.state = Complete; + Ok(Token { + value: op, + kind: TokenKind::Operator, + }) + } else { + Err(Syntax(format!( + "unrecognized operator {} at index: {}", + char_at(self.i, sql), + self.i + ))) + } + } + + fn tokenize_number(&mut self, sql: &str) -> Result { + let start = self.i; + + let mut c = char_at(self.i, sql); + while self.i < sql.len() && c.is_ascii_digit() { + self.i += c.len_utf8(); + c = char_at(self.i, sql); + } + + let number = &sql[start..self.i]; + self.state = Complete; + Ok(Token { + value: String::from(number), + kind: Number, + }) + } + + fn tokenize_quoted(&mut self, sql: &str) -> Result { + let start = self.i; + for (index, char) in sql[start..].char_indices() { + if char == '\'' { + self.i = start + index + 1; + let quoted = &sql[start..start + index]; + self.state = Complete; + return Ok(Token { + value: String::from(quoted), + kind: TokenKind::String, + }); + } + } + + Err(Syntax(format!( + "unclosed quotation mark after index: {}", + start + ))) + } + + fn matches_keyword(&self, sql: &str) -> Option { + for word in reserved_keywords() { + let end = self.i + word.len(); + if end <= sql.len() { + let token = &sql[self.i..end]; + let next_char = char_at(end, sql); + if token.to_uppercase() == word.as_str() && !is_identifier_char(next_char) { + return Some(word); + } + } + } + None + } + + fn matches_operator(&self, sql: &str) -> Option { + for op in valid_operators() { + let end = self.i + op.len(); + if end <= sql.len() { + let token = &sql[self.i..end]; + if token == op.as_str() && !is_operator_char(char_at(end, sql)) { + return Some(op); + } + } + } + None + } + + fn reset(&mut self) { + self.state = Begin + } +} From bbc7a86ce4c3f39c4e020dd17d64f22009936f11 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 2 Sep 2024 21:52:29 -0300 Subject: [PATCH 018/103] starting builder module --- src/query/builder.rs | 15 +++++++ src/query/mod.rs | 94 +++++++++++++++++++++++++++++++++++++----- src/query/tokenizer.rs | 9 ++-- 3 files changed, 104 insertions(+), 14 deletions(-) diff --git a/src/query/builder.rs b/src/query/builder.rs index 8b13789..a88822c 100644 --- a/src/query/builder.rs +++ b/src/query/builder.rs @@ -1 +1,16 @@ +use crate::query::errors::InvalidSQL; +use crate::query::{Query, Token}; +pub struct Builder { + pub tokens: Vec, +} + +impl Builder { + pub fn default() -> Self { + Self { tokens: vec![] } + } + + pub fn build(tokens: Vec) -> Result { + Ok(Query::default()) + } +} diff --git a/src/query/mod.rs b/src/query/mod.rs index 5797953..99ca6cc 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -4,23 +4,24 @@ mod executor; pub mod tokenizer; use crate::errors::Errored; +use crate::query::Ordering::Asc; +use crate::query::StatementKind::Condition; use crate::query::TokenKind::Unknown; +pub struct Query { + pub operation: Operation, + pub table: String, + fields: Option>, + expressions: Option>, + ordering: Order, +} + #[derive(Debug)] pub struct Token { pub value: String, pub kind: TokenKind, } -impl Token { - pub fn new() -> Self { - Self { - value: String::new(), - kind: Unknown, - } - } -} - #[derive(Debug, PartialEq)] pub enum TokenKind { Unknown, @@ -31,8 +32,80 @@ pub enum TokenKind { Keyword, } +#[derive(Debug, PartialEq)] +pub enum Operation { + Unknown, + Select, + Update, + Delete, + Insert, +} + +struct Statement { + kind: StatementKind, + operator: Token, + left: Token, + right: Token, +} + #[derive(Debug)] -pub struct Query {} +struct Order { + fields: Vec, + order: Ordering, +} + +#[derive(Debug, PartialEq)] +enum StatementKind { + Condition, + Assignment, +} + +#[derive(Debug)] +enum Ordering { + Asc, + Desc, +} + +impl Order { + pub fn default() -> Self { + Self { + fields: vec![], + order: Asc, + } + } +} + +impl Statement { + pub fn default() -> Self { + Self { + kind: Condition, + operator: Token::default(), + left: Token::default(), + right: Token::default(), + } + } +} + +impl Token { + pub fn default() -> Self { + Self { + value: String::new(), + kind: Unknown, + } + } +} + +impl Query { + pub fn default() -> Self { + Self { + operation: Operation::Unknown, + table: "".to_string(), + fields: None, + expressions: None, + ordering: Order::default(), + } + } +} fn char_at(index: usize, string: &str) -> char { string[index..].chars().next().unwrap_or('\0') @@ -81,6 +154,7 @@ fn reserved_keywords() -> Vec { "WHERE".to_string(), "AND".to_string(), "OR".to_string(), + "NOT".to_string(), ] } diff --git a/src/query/tokenizer.rs b/src/query/tokenizer.rs index 2538ec8..a2c2b6b 100644 --- a/src/query/tokenizer.rs +++ b/src/query/tokenizer.rs @@ -28,7 +28,7 @@ impl Tokenizer { pub fn tokenize(&mut self, sql: &str) -> Result, InvalidSQL> { let mut out = vec![]; - let mut token = Token::new(); + let mut token = Token::default(); while self.i < sql.len() { let c = char_at(self.i, sql); match self.state { @@ -42,7 +42,7 @@ impl Tokenizer { } Complete => { out.push(token); - token = Token::new(); + token = Token::default(); self.reset() } } @@ -133,8 +133,9 @@ impl Tokenizer { let start = self.i; for (index, char) in sql[start..].char_indices() { if char == '\'' { - self.i = start + index + 1; - let quoted = &sql[start..start + index]; + let end = start + index; + self.i = end + 1; + let quoted = &sql[start..end]; self.state = Complete; return Ok(Token { value: String::from(quoted), From ae6ea14354ff55305c9c2972b59bac3ba34570c0 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 2 Sep 2024 22:02:09 -0300 Subject: [PATCH 019/103] module refactoring --- src/query/mod.rs | 77 +++++++----------------------------------- src/query/tokenizer.rs | 56 +++++++++++++++++++++++++++--- 2 files changed, 65 insertions(+), 68 deletions(-) diff --git a/src/query/mod.rs b/src/query/mod.rs index 99ca6cc..c4f8464 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -22,6 +22,19 @@ pub struct Token { pub kind: TokenKind, } +struct Statement { + kind: StatementKind, + operator: Token, + left: Token, + right: Token, +} + +#[derive(Debug)] +struct Order { + fields: Vec, + order: Ordering, +} + #[derive(Debug, PartialEq)] pub enum TokenKind { Unknown, @@ -41,19 +54,6 @@ pub enum Operation { Insert, } -struct Statement { - kind: StatementKind, - operator: Token, - left: Token, - right: Token, -} - -#[derive(Debug)] -struct Order { - fields: Vec, - order: Ordering, -} - #[derive(Debug, PartialEq)] enum StatementKind { Condition, @@ -107,57 +107,6 @@ impl Query { } } -fn char_at(index: usize, string: &str) -> char { - string[index..].chars().next().unwrap_or('\0') -} - -fn can_be_skipped(c: char) -> bool { - c.is_whitespace() || ignorable_chars().contains(&c) -} - -fn is_identifier_char(c: char) -> bool { - c == '_' || c.is_alphanumeric() && !can_be_skipped(c) -} - -fn is_operator_char(c: char) -> bool { - valid_operators().contains(&c.to_string()) -} - -fn valid_operators() -> Vec { - vec![ - "*".to_string(), - "=".to_string(), - "<".to_string(), - ">".to_string(), - ">=".to_string(), - "<=".to_string(), - "!=".to_string(), - "<>".to_string(), - ] -} - -fn ignorable_chars() -> Vec { - vec![' ', '(', ')', ',', ';', '\0', '\n'] -} - -fn reserved_keywords() -> Vec { - vec![ - "SELECT".to_string(), - "UPDATE".to_string(), - "DELETE".to_string(), - "INSERT INTO".to_string(), - "VALUES".to_string(), - "ORDER BY".to_string(), - "DESC".to_string(), - "ASC".to_string(), - "FROM".to_string(), - "WHERE".to_string(), - "AND".to_string(), - "OR".to_string(), - "NOT".to_string(), - ] -} - pub fn validate_query_string(query: &str) -> Result<(), Errored> { if query.trim().is_empty() { return Err(Errored(String::from("query is empty."))); diff --git a/src/query/tokenizer.rs b/src/query/tokenizer.rs index a2c2b6b..7ac07ca 100644 --- a/src/query/tokenizer.rs +++ b/src/query/tokenizer.rs @@ -2,10 +2,7 @@ use crate::query::errors::InvalidSQL; use crate::query::errors::InvalidSQL::Syntax; use crate::query::tokenizer::TokenizerState::*; use crate::query::TokenKind::{Identifier, Keyword, Number, Unknown}; -use crate::query::{ - can_be_skipped, char_at, is_identifier_char, is_operator_char, reserved_keywords, - valid_operators, Token, TokenKind, -}; +use crate::query::{Token, TokenKind}; pub struct Tokenizer { i: usize, @@ -181,3 +178,54 @@ impl Tokenizer { self.state = Begin } } + +fn char_at(index: usize, string: &str) -> char { + string[index..].chars().next().unwrap_or('\0') +} + +fn can_be_skipped(c: char) -> bool { + c.is_whitespace() || ignorable_chars().contains(&c) +} + +fn is_identifier_char(c: char) -> bool { + c == '_' || c.is_alphanumeric() && !can_be_skipped(c) +} + +fn is_operator_char(c: char) -> bool { + valid_operators().contains(&c.to_string()) +} + +fn valid_operators() -> Vec { + vec![ + "*".to_string(), + "=".to_string(), + "<".to_string(), + ">".to_string(), + ">=".to_string(), + "<=".to_string(), + "!=".to_string(), + "<>".to_string(), + ] +} + +fn ignorable_chars() -> Vec { + vec![' ', '(', ')', ',', ';', '\0', '\n'] +} + +fn reserved_keywords() -> Vec { + vec![ + "SELECT".to_string(), + "UPDATE".to_string(), + "DELETE".to_string(), + "INSERT INTO".to_string(), + "VALUES".to_string(), + "ORDER BY".to_string(), + "DESC".to_string(), + "ASC".to_string(), + "FROM".to_string(), + "WHERE".to_string(), + "AND".to_string(), + "OR".to_string(), + "NOT".to_string(), + ] +} From 1904f6aeb4da93706a56873d49c23b63e73a53c4 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 2 Sep 2024 22:55:03 -0300 Subject: [PATCH 020/103] new error macro and used generics to reduce repetition in tokenizer --- src/errors.rs | 10 ++++ src/files/mod.rs | 7 +-- src/query/tokenizer.rs | 107 +++++++++++++++++++---------------------- 3 files changed, 64 insertions(+), 60 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index 0e60fbe..6d9aad1 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,6 +1,16 @@ use std::error::Error; use std::fmt::{Debug, Display, Formatter}; +#[macro_export] +macro_rules! errored { + ($err_type:ident, $msg:expr) => { + return Err($err_type(format!($msg))) + }; + ($err_type:ident, $fmt:expr, $($arg:tt)*) => { + return Err($err_type(format!($fmt, $($arg)*))) + }; +} + /// Generic Error for the RusticSQL Application. pub struct Errored(pub String); diff --git a/src/files/mod.rs b/src/files/mod.rs index d66b834..d75c693 100644 --- a/src/files/mod.rs +++ b/src/files/mod.rs @@ -1,3 +1,4 @@ +use crate::errored; use crate::errors::Errored; use std::path::Path; @@ -5,17 +6,17 @@ pub fn validate_path(dir: &str) -> Result<(), Errored> { let path = Path::new(dir); if !path.exists() { - return Err(Errored(format!("path '{dir}' does not exist"))); + errored!(Errored, "path '{dir}' does not exist"); } if !path.is_dir() { - return Err(Errored(format!("path '{dir}' is not a valid directory"))); + errored!(Errored, "path '{dir}' is not a valid directory"); } let dir_entries = path .read_dir() .map_err(|e| Errored(format!("failure when reading directory '{dir}': {e}")))?; if dir_entries.count() == 0 { - return Err(Errored(format!("path '{dir}' is an empty directory"))); + errored!(Errored, "path '{dir}' is an empty directory"); } Ok(()) } diff --git a/src/query/tokenizer.rs b/src/query/tokenizer.rs index 7ac07ca..8269518 100644 --- a/src/query/tokenizer.rs +++ b/src/query/tokenizer.rs @@ -1,3 +1,4 @@ +use crate::errored; use crate::query::errors::InvalidSQL; use crate::query::errors::InvalidSQL::Syntax; use crate::query::tokenizer::TokenizerState::*; @@ -57,18 +58,12 @@ impl Tokenizer { c if is_identifier_char(c) => self.state = IdentifierOrKeyword, '\'' => self.state = StringLiteral, c if is_operator_char(c) => self.state = Operator, - _ => { - return Err(Syntax(format!( - "could not tokenize char: {} at index: {}.", - c, self.i - ))) - } + _ => errored!(Syntax, "could not tokenize char: {} at index: {}.", c, self.i) } Ok(()) } fn tokenize_identifier_or_keyword(&mut self, sql: &str) -> Result { - let start = self.i; if let Some(word) = self.matches_keyword(sql) { self.i += word.len(); self.state = Complete; @@ -77,19 +72,11 @@ impl Tokenizer { kind: Keyword, }); } + self.tokenize_kind(sql, Identifier, is_identifier_char) + } - let mut c = char_at(self.i, sql); - while self.i < sql.len() && is_identifier_char(c) { - self.i += c.len_utf8(); - c = char_at(self.i, sql); - } - - let identifier = &sql[start..self.i]; - self.state = Complete; - Ok(Token { - value: String::from(identifier), - kind: Identifier, - }) + fn tokenize_number(&mut self, sql: &str) -> Result { + self.tokenize_kind(sql, Number, |c| c.is_ascii_digit()) } fn tokenize_operator(&mut self, sql: &str) -> Result { @@ -101,31 +88,15 @@ impl Tokenizer { kind: TokenKind::Operator, }) } else { - Err(Syntax(format!( + errored!( + Syntax, "unrecognized operator {} at index: {}", char_at(self.i, sql), self.i - ))) + ); } } - fn tokenize_number(&mut self, sql: &str) -> Result { - let start = self.i; - - let mut c = char_at(self.i, sql); - while self.i < sql.len() && c.is_ascii_digit() { - self.i += c.len_utf8(); - c = char_at(self.i, sql); - } - - let number = &sql[start..self.i]; - self.state = Complete; - Ok(Token { - value: String::from(number), - kind: Number, - }) - } - fn tokenize_quoted(&mut self, sql: &str) -> Result { let start = self.i; for (index, char) in sql[start..].char_indices() { @@ -140,38 +111,60 @@ impl Tokenizer { }); } } - - Err(Syntax(format!( - "unclosed quotation mark after index: {}", - start - ))) + errored!(Syntax, "unclosed quotation mark after index: {start}"); } fn matches_keyword(&self, sql: &str) -> Option { - for word in reserved_keywords() { - let end = self.i + word.len(); + self.matches_special_tokens(sql, &reserved_keywords(), is_identifier_char) + } + + fn matches_operator(&self, sql: &str) -> Option { + self.matches_special_tokens(sql, &valid_operators(), is_operator_char) + } + + fn matches_special_tokens( + &self, + sql: &str, + tokens: &[String], + matches_kind: F, + ) -> Option + where + F: Fn(char) -> bool, + { + for t in tokens { + let end = self.i + t.len(); if end <= sql.len() { let token = &sql[self.i..end]; let next_char = char_at(end, sql); - if token.to_uppercase() == word.as_str() && !is_identifier_char(next_char) { - return Some(word); + if token.to_uppercase() == t.to_uppercase() && !matches_kind(next_char) { + return Some(token.to_uppercase()); } } } None } - fn matches_operator(&self, sql: &str) -> Option { - for op in valid_operators() { - let end = self.i + op.len(); - if end <= sql.len() { - let token = &sql[self.i..end]; - if token == op.as_str() && !is_operator_char(char_at(end, sql)) { - return Some(op); - } - } + fn tokenize_kind( + &mut self, + sql: &str, + output_kind: TokenKind, + matches_kind: F, + ) -> Result + where + F: Fn(char) -> bool, + { + let start = self.i; + let mut c = char_at(self.i, sql); + while self.i < sql.len() && matches_kind(c) { + self.i += c.len_utf8(); + c = char_at(self.i, sql); } - None + let token = &sql[start..self.i]; + self.state = Complete; + Ok(Token { + value: String::from(token), + kind: output_kind, + }) } fn reset(&mut self) { From c7cd81e9bea97cda03d7c8996cc32e0ce203d824 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 2 Sep 2024 23:00:17 -0300 Subject: [PATCH 021/103] formatting --- src/query/tokenizer.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/query/tokenizer.rs b/src/query/tokenizer.rs index 8269518..f0ce819 100644 --- a/src/query/tokenizer.rs +++ b/src/query/tokenizer.rs @@ -58,7 +58,12 @@ impl Tokenizer { c if is_identifier_char(c) => self.state = IdentifierOrKeyword, '\'' => self.state = StringLiteral, c if is_operator_char(c) => self.state = Operator, - _ => errored!(Syntax, "could not tokenize char: {} at index: {}.", c, self.i) + _ => errored!( + Syntax, + "could not tokenize char: {} at index: {}.", + c, + self.i + ), } Ok(()) } From 1b8c1e2690b4cda52d833aa2e54f9f0875bdef3a Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 2 Sep 2024 23:45:51 -0300 Subject: [PATCH 022/103] made validate_path simpler --- src/errors.rs | 7 +++++++ src/files/mod.rs | 11 +++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index 6d9aad1..7633b8b 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,5 +1,6 @@ use std::error::Error; use std::fmt::{Debug, Display, Formatter}; +use std::io; #[macro_export] macro_rules! errored { @@ -27,3 +28,9 @@ impl Display for Errored { write!(f, "[ERROR]: {}", self.0) } } + +impl From for Errored { + fn from(value: io::Error) -> Self { + Errored(format!("IO Error: {}", value)) + } +} diff --git a/src/files/mod.rs b/src/files/mod.rs index d75c693..166ec36 100644 --- a/src/files/mod.rs +++ b/src/files/mod.rs @@ -7,16 +7,11 @@ pub fn validate_path(dir: &str) -> Result<(), Errored> { if !path.exists() { errored!(Errored, "path '{dir}' does not exist"); - } - if !path.is_dir() { + } else if !path.is_dir() { errored!(Errored, "path '{dir}' is not a valid directory"); - } - - let dir_entries = path - .read_dir() - .map_err(|e| Errored(format!("failure when reading directory '{dir}': {e}")))?; - if dir_entries.count() == 0 { + } else if path.read_dir()?.next().is_none() { errored!(Errored, "path '{dir}' is an empty directory"); } + Ok(()) } From 2762937c64b4d715d71a477ec07ed5f9cb661a1e Mon Sep 17 00:00:00 2001 From: gabokatta Date: Thu, 5 Sep 2024 01:55:22 -0300 Subject: [PATCH 023/103] layed groundwork for query builders --- src/main.rs | 10 ++++-- src/query/builder.rs | 16 --------- src/query/builder/delete.rs | 22 ++++++++++++ src/query/builder/insert.rs | 22 ++++++++++++ src/query/builder/mod.rs | 46 +++++++++++++++++++++++++ src/query/builder/select.rs | 28 ++++++++++++++++ src/query/builder/update.rs | 22 ++++++++++++ src/query/mod.rs | 4 ++- src/query/tokenizer.rs | 67 ++++++++++++++----------------------- 9 files changed, 177 insertions(+), 60 deletions(-) delete mode 100644 src/query/builder.rs create mode 100644 src/query/builder/delete.rs create mode 100644 src/query/builder/insert.rs create mode 100644 src/query/builder/mod.rs create mode 100644 src/query/builder/select.rs create mode 100644 src/query/builder/update.rs diff --git a/src/main.rs b/src/main.rs index d1ca5a7..5e6e3f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use crate::query::tokenizer::Tokenizer; +use crate::query::Query; use files::validate_path; use query::validate_query_string; use std::env; @@ -29,9 +30,14 @@ fn run(args: Vec) -> Result<(), Box> { validate_query_string(query)?; let mut tokenizer = Tokenizer::new(); + let tokens = tokenizer.tokenize(query)?; - for t in tokens { - println!("{:?}", t) + for t in &tokens { + println!("{:?}", &t) } + + let query = Query::from(tokens)?; + println!("\n{:?}", &query); + Ok(()) } diff --git a/src/query/builder.rs b/src/query/builder.rs deleted file mode 100644 index a88822c..0000000 --- a/src/query/builder.rs +++ /dev/null @@ -1,16 +0,0 @@ -use crate::query::errors::InvalidSQL; -use crate::query::{Query, Token}; - -pub struct Builder { - pub tokens: Vec, -} - -impl Builder { - pub fn default() -> Self { - Self { tokens: vec![] } - } - - pub fn build(tokens: Vec) -> Result { - Ok(Query::default()) - } -} diff --git a/src/query/builder/delete.rs b/src/query/builder/delete.rs new file mode 100644 index 0000000..4470ff5 --- /dev/null +++ b/src/query/builder/delete.rs @@ -0,0 +1,22 @@ +use crate::query::builder::Builder; +use crate::query::errors::InvalidSQL; +use crate::query::{Query, Token}; +use std::collections::VecDeque; + +const ALLOWED_KEYWORDS: &[&str] = &["FROM", "WHERE", "AND", "OR"]; + +pub struct DeleteBuilder { + tokens: VecDeque, +} + +impl DeleteBuilder { + pub fn new(tokens: VecDeque) -> Self { + Self { tokens } + } +} + +impl Builder for DeleteBuilder { + fn build(&mut self) -> Result { + todo!() + } +} diff --git a/src/query/builder/insert.rs b/src/query/builder/insert.rs new file mode 100644 index 0000000..85ef8d4 --- /dev/null +++ b/src/query/builder/insert.rs @@ -0,0 +1,22 @@ +use crate::query::builder::Builder; +use crate::query::errors::InvalidSQL; +use crate::query::{Query, Token}; +use std::collections::VecDeque; + +const ALLOWED_KEYWORDS: &[&str] = &["VALUES"]; + +pub struct InsertBuilder { + tokens: VecDeque, +} + +impl InsertBuilder { + pub fn new(tokens: VecDeque) -> Self { + Self { tokens } + } +} + +impl Builder for InsertBuilder { + fn build(&mut self) -> Result { + todo!() + } +} diff --git a/src/query/builder/mod.rs b/src/query/builder/mod.rs new file mode 100644 index 0000000..e3561d7 --- /dev/null +++ b/src/query/builder/mod.rs @@ -0,0 +1,46 @@ +mod delete; +mod insert; +mod select; +mod update; + +use crate::errored; +use crate::query::builder::delete::DeleteBuilder; +use crate::query::builder::insert::InsertBuilder; +use crate::query::builder::select::SelectBuilder; +use crate::query::builder::update::UpdateBuilder; +use crate::query::errors::InvalidSQL; +use crate::query::errors::InvalidSQL::Syntax; +use crate::query::Operation::{Delete, Insert, Select, Unknown, Update}; +use crate::query::{Operation, Query, Token}; +use std::collections::VecDeque; + +pub trait Builder { + fn build(&mut self) -> Result; +} + +impl Query { + pub fn from(tokens: Vec) -> Result { + let mut tokens = VecDeque::from(tokens); + let kind = get_kind(tokens.pop_front()); + match kind { + Unknown => errored!(Syntax, "query does not start with a valid operation."), + Select => SelectBuilder::new(tokens).build(), + Update => UpdateBuilder::new(tokens).build(), + Delete => DeleteBuilder::new(tokens).build(), + Insert => InsertBuilder::new(tokens).build(), + } + } +} + +fn get_kind(token: Option) -> Operation { + match token { + Some(t) => match t.value.as_str() { + "SELECT" => Select, + "INSERT INTO" => Insert, + "UPDATE" => Update, + "DELETE" => Delete, + _ => Unknown, + }, + None => Unknown, + } +} diff --git a/src/query/builder/select.rs b/src/query/builder/select.rs new file mode 100644 index 0000000..69c4df4 --- /dev/null +++ b/src/query/builder/select.rs @@ -0,0 +1,28 @@ +use crate::query::builder::Builder; +use crate::query::errors::InvalidSQL; +use crate::query::Operation::Select; +use crate::query::{Query, Token}; +use std::collections::VecDeque; + +const ALLOWED_KEYWORDS: &[&str] = &[ + "SELECT", "FROM", "WHERE", "ORDER_BY", "ASC", "DESC", "AND", "OR", +]; + +pub struct SelectBuilder { + tokens: VecDeque, +} + +impl SelectBuilder { + pub fn new(tokens: VecDeque) -> Self { + Self { tokens } + } +} + +impl Builder for SelectBuilder { + fn build(&mut self) -> Result { + let mut query = Query::default(); + query.operation = Select; + + Ok(query) + } +} diff --git a/src/query/builder/update.rs b/src/query/builder/update.rs new file mode 100644 index 0000000..04dc1d1 --- /dev/null +++ b/src/query/builder/update.rs @@ -0,0 +1,22 @@ +use crate::query::builder::Builder; +use crate::query::errors::InvalidSQL; +use crate::query::{Query, Token}; +use std::collections::VecDeque; + +const ALLOWED_KEYWORDS: &[&str] = &["SET", "WHERE", "AND", "OR"]; + +pub struct UpdateBuilder { + tokens: VecDeque, +} + +impl UpdateBuilder { + pub fn new(tokens: VecDeque) -> Self { + Self { tokens } + } +} + +impl Builder for UpdateBuilder { + fn build(&mut self) -> Result { + todo!() + } +} diff --git a/src/query/mod.rs b/src/query/mod.rs index c4f8464..284b6bb 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -1,4 +1,4 @@ -mod builder; +pub mod builder; mod errors; mod executor; pub mod tokenizer; @@ -8,6 +8,7 @@ use crate::query::Ordering::Asc; use crate::query::StatementKind::Condition; use crate::query::TokenKind::Unknown; +#[derive(Debug)] pub struct Query { pub operation: Operation, pub table: String, @@ -22,6 +23,7 @@ pub struct Token { pub kind: TokenKind, } +#[derive(Debug)] struct Statement { kind: StatementKind, operator: Token, diff --git a/src/query/tokenizer.rs b/src/query/tokenizer.rs index f0ce819..ef644fc 100644 --- a/src/query/tokenizer.rs +++ b/src/query/tokenizer.rs @@ -5,6 +5,26 @@ use crate::query::tokenizer::TokenizerState::*; use crate::query::TokenKind::{Identifier, Keyword, Number, Unknown}; use crate::query::{Token, TokenKind}; +const VALID_OPERATORS: &[&str] = &["*", "=", "<", ">", ">=", "<=", "!=", "<>"]; + +const IGNORABLE_CHARS: &[char] = &[' ', '(', ')', ',', ';', '\0', '\n']; + +const RESERVED_KEYWORDS: &[&str] = &[ + "SELECT", + "UPDATE", + "DELETE", + "INSERT INTO", + "VALUES", + "ORDER BY", + "DESC", + "ASC", + "FROM", + "WHERE", + "AND", + "OR", + "NOT", +]; + pub struct Tokenizer { i: usize, state: TokenizerState, @@ -120,17 +140,17 @@ impl Tokenizer { } fn matches_keyword(&self, sql: &str) -> Option { - self.matches_special_tokens(sql, &reserved_keywords(), is_identifier_char) + self.matches_special_tokens(sql, RESERVED_KEYWORDS, is_identifier_char) } fn matches_operator(&self, sql: &str) -> Option { - self.matches_special_tokens(sql, &valid_operators(), is_operator_char) + self.matches_special_tokens(sql, VALID_OPERATORS, is_operator_char) } fn matches_special_tokens( &self, sql: &str, - tokens: &[String], + tokens: &[&str], matches_kind: F, ) -> Option where @@ -182,48 +202,13 @@ fn char_at(index: usize, string: &str) -> char { } fn can_be_skipped(c: char) -> bool { - c.is_whitespace() || ignorable_chars().contains(&c) + c.is_whitespace() || IGNORABLE_CHARS.contains(&c) } fn is_identifier_char(c: char) -> bool { - c == '_' || c.is_alphanumeric() && !can_be_skipped(c) + c == '_' || (c.is_alphanumeric() && !can_be_skipped(c)) } fn is_operator_char(c: char) -> bool { - valid_operators().contains(&c.to_string()) -} - -fn valid_operators() -> Vec { - vec![ - "*".to_string(), - "=".to_string(), - "<".to_string(), - ">".to_string(), - ">=".to_string(), - "<=".to_string(), - "!=".to_string(), - "<>".to_string(), - ] -} - -fn ignorable_chars() -> Vec { - vec![' ', '(', ')', ',', ';', '\0', '\n'] -} - -fn reserved_keywords() -> Vec { - vec![ - "SELECT".to_string(), - "UPDATE".to_string(), - "DELETE".to_string(), - "INSERT INTO".to_string(), - "VALUES".to_string(), - "ORDER BY".to_string(), - "DESC".to_string(), - "ASC".to_string(), - "FROM".to_string(), - "WHERE".to_string(), - "AND".to_string(), - "OR".to_string(), - "NOT".to_string(), - ] + VALID_OPERATORS.contains(&c.to_string().as_str()) } From e2bebfc484be7b80957461404a2b6463c6ecfc61 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Thu, 5 Sep 2024 02:26:45 -0300 Subject: [PATCH 024/103] keyword validation and executor module refactor --- src/query/builder/delete.rs | 12 +++++++++-- src/query/builder/insert.rs | 12 +++++++++-- src/query/builder/mod.rs | 21 +++++++++++++++++++ src/query/builder/select.rs | 7 ++++++- src/query/builder/update.rs | 13 ++++++++++-- src/query/{executor.rs => executor/delete.rs} | 0 src/query/executor/insert.rs | 1 + src/query/executor/mod.rs | 4 ++++ src/query/executor/select.rs | 1 + src/query/executor/update.rs | 1 + 10 files changed, 65 insertions(+), 7 deletions(-) rename src/query/{executor.rs => executor/delete.rs} (100%) create mode 100644 src/query/executor/insert.rs create mode 100644 src/query/executor/mod.rs create mode 100644 src/query/executor/select.rs create mode 100644 src/query/executor/update.rs diff --git a/src/query/builder/delete.rs b/src/query/builder/delete.rs index 4470ff5..2f56c52 100644 --- a/src/query/builder/delete.rs +++ b/src/query/builder/delete.rs @@ -1,5 +1,6 @@ -use crate::query::builder::Builder; +use crate::query::builder::{validate_keywords, Builder}; use crate::query::errors::InvalidSQL; +use crate::query::Operation::Delete; use crate::query::{Query, Token}; use std::collections::VecDeque; @@ -17,6 +18,13 @@ impl DeleteBuilder { impl Builder for DeleteBuilder { fn build(&mut self) -> Result { - todo!() + let mut query = Query::default(); + query.operation = Delete; + self.validate_keywords()?; + + Ok(query) + } + fn validate_keywords(&self) -> Result<(), InvalidSQL> { + validate_keywords(ALLOWED_KEYWORDS, &self.tokens, Delete) } } diff --git a/src/query/builder/insert.rs b/src/query/builder/insert.rs index 85ef8d4..8996c1e 100644 --- a/src/query/builder/insert.rs +++ b/src/query/builder/insert.rs @@ -1,5 +1,6 @@ -use crate::query::builder::Builder; +use crate::query::builder::{validate_keywords, Builder}; use crate::query::errors::InvalidSQL; +use crate::query::Operation::Insert; use crate::query::{Query, Token}; use std::collections::VecDeque; @@ -17,6 +18,13 @@ impl InsertBuilder { impl Builder for InsertBuilder { fn build(&mut self) -> Result { - todo!() + let mut query = Query::default(); + query.operation = Insert; + self.validate_keywords()?; + + Ok(query) + } + fn validate_keywords(&self) -> Result<(), InvalidSQL> { + validate_keywords(ALLOWED_KEYWORDS, &self.tokens, Insert) } } diff --git a/src/query/builder/mod.rs b/src/query/builder/mod.rs index e3561d7..893f58f 100644 --- a/src/query/builder/mod.rs +++ b/src/query/builder/mod.rs @@ -11,11 +11,13 @@ use crate::query::builder::update::UpdateBuilder; use crate::query::errors::InvalidSQL; use crate::query::errors::InvalidSQL::Syntax; use crate::query::Operation::{Delete, Insert, Select, Unknown, Update}; +use crate::query::TokenKind::Keyword; use crate::query::{Operation, Query, Token}; use std::collections::VecDeque; pub trait Builder { fn build(&mut self) -> Result; + fn validate_keywords(&self) -> Result<(), InvalidSQL>; } impl Query { @@ -44,3 +46,22 @@ fn get_kind(token: Option) -> Operation { None => Unknown, } } + +fn validate_keywords( + allowed: &[&str], + tokens: &VecDeque, + operation: Operation, +) -> Result<(), InvalidSQL> { + let keywords: VecDeque<&Token> = tokens.iter().filter(|t| t.kind == Keyword).collect(); + for word in keywords { + if !allowed.contains(&&*word.value) { + errored!( + Syntax, + "invalid keyword for {:?} query detected: {}", + operation, + word.value + ) + } + } + Ok(()) +} diff --git a/src/query/builder/select.rs b/src/query/builder/select.rs index 69c4df4..112c0bc 100644 --- a/src/query/builder/select.rs +++ b/src/query/builder/select.rs @@ -1,4 +1,4 @@ -use crate::query::builder::Builder; +use crate::query::builder::{validate_keywords, Builder}; use crate::query::errors::InvalidSQL; use crate::query::Operation::Select; use crate::query::{Query, Token}; @@ -22,7 +22,12 @@ impl Builder for SelectBuilder { fn build(&mut self) -> Result { let mut query = Query::default(); query.operation = Select; + self.validate_keywords()?; Ok(query) } + + fn validate_keywords(&self) -> Result<(), InvalidSQL> { + validate_keywords(ALLOWED_KEYWORDS, &self.tokens, Select) + } } diff --git a/src/query/builder/update.rs b/src/query/builder/update.rs index 04dc1d1..0473b9f 100644 --- a/src/query/builder/update.rs +++ b/src/query/builder/update.rs @@ -1,5 +1,6 @@ -use crate::query::builder::Builder; +use crate::query::builder::{validate_keywords, Builder}; use crate::query::errors::InvalidSQL; +use crate::query::Operation::Update; use crate::query::{Query, Token}; use std::collections::VecDeque; @@ -17,6 +18,14 @@ impl UpdateBuilder { impl Builder for UpdateBuilder { fn build(&mut self) -> Result { - todo!() + let mut query = Query::default(); + query.operation = Update; + self.validate_keywords()?; + + Ok(query) + } + + fn validate_keywords(&self) -> Result<(), InvalidSQL> { + validate_keywords(ALLOWED_KEYWORDS, &self.tokens, Update) } } diff --git a/src/query/executor.rs b/src/query/executor/delete.rs similarity index 100% rename from src/query/executor.rs rename to src/query/executor/delete.rs diff --git a/src/query/executor/insert.rs b/src/query/executor/insert.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/query/executor/insert.rs @@ -0,0 +1 @@ + diff --git a/src/query/executor/mod.rs b/src/query/executor/mod.rs new file mode 100644 index 0000000..24b704e --- /dev/null +++ b/src/query/executor/mod.rs @@ -0,0 +1,4 @@ +mod delete; +mod insert; +mod select; +mod update; diff --git a/src/query/executor/select.rs b/src/query/executor/select.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/query/executor/select.rs @@ -0,0 +1 @@ + diff --git a/src/query/executor/update.rs b/src/query/executor/update.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/query/executor/update.rs @@ -0,0 +1 @@ + From 044030631060b23b073e9508edeb8e9e0dc6572e Mon Sep 17 00:00:00 2001 From: gabokatta Date: Thu, 5 Sep 2024 02:28:50 -0300 Subject: [PATCH 025/103] added SET to keywords --- src/query/tokenizer.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/query/tokenizer.rs b/src/query/tokenizer.rs index ef644fc..21d21b4 100644 --- a/src/query/tokenizer.rs +++ b/src/query/tokenizer.rs @@ -14,6 +14,7 @@ const RESERVED_KEYWORDS: &[&str] = &[ "UPDATE", "DELETE", "INSERT INTO", + "SET", "VALUES", "ORDER BY", "DESC", From c3025a4f7b5b4d8dca6d48f60bc1cba1c42959eb Mon Sep 17 00:00:00 2001 From: gabokatta Date: Thu, 5 Sep 2024 02:45:30 -0300 Subject: [PATCH 026/103] began select builder --- src/query/builder/select.rs | 25 +++++++++++++++++++++++-- src/query/mod.rs | 24 ++++++++++++------------ 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/query/builder/select.rs b/src/query/builder/select.rs index 112c0bc..6fb2d5c 100644 --- a/src/query/builder/select.rs +++ b/src/query/builder/select.rs @@ -1,7 +1,7 @@ use crate::query::builder::{validate_keywords, Builder}; use crate::query::errors::InvalidSQL; use crate::query::Operation::Select; -use crate::query::{Query, Token}; +use crate::query::{Ordering, Query, Statement, Token}; use std::collections::VecDeque; const ALLOWED_KEYWORDS: &[&str] = &[ @@ -16,14 +16,35 @@ impl SelectBuilder { pub fn new(tokens: VecDeque) -> Self { Self { tokens } } + + fn process_table(&self) -> String { + "".to_string() + } + + fn process_fields(&self) -> Vec { + vec![] + } + + fn process_ordering(&self) -> Ordering { + Ordering::default() + } + + fn process_expressions(&self) -> Vec { + vec![] + } } impl Builder for SelectBuilder { fn build(&mut self) -> Result { let mut query = Query::default(); - query.operation = Select; self.validate_keywords()?; + query.operation = Select; + query.table = self.process_table(); + query.fields = self.process_fields(); + query.expressions = self.process_expressions(); + query.ordering = self.process_ordering(); + Ok(query) } diff --git a/src/query/mod.rs b/src/query/mod.rs index 284b6bb..fa1d704 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -4,7 +4,7 @@ mod executor; pub mod tokenizer; use crate::errors::Errored; -use crate::query::Ordering::Asc; +use crate::query::OrderKind::Asc; use crate::query::StatementKind::Condition; use crate::query::TokenKind::Unknown; @@ -12,9 +12,9 @@ use crate::query::TokenKind::Unknown; pub struct Query { pub operation: Operation, pub table: String, - fields: Option>, - expressions: Option>, - ordering: Order, + fields: Vec, + expressions: Vec, + ordering: Ordering, } #[derive(Debug)] @@ -32,9 +32,9 @@ struct Statement { } #[derive(Debug)] -struct Order { +struct Ordering { fields: Vec, - order: Ordering, + kind: OrderKind, } #[derive(Debug, PartialEq)] @@ -63,16 +63,16 @@ enum StatementKind { } #[derive(Debug)] -enum Ordering { +enum OrderKind { Asc, Desc, } -impl Order { +impl Ordering { pub fn default() -> Self { Self { fields: vec![], - order: Asc, + kind: Asc, } } } @@ -102,9 +102,9 @@ impl Query { Self { operation: Operation::Unknown, table: "".to_string(), - fields: None, - expressions: None, - ordering: Order::default(), + fields: vec![], + expressions: vec![], + ordering: Ordering::default(), } } } From 0e49beb056986cb00e279257ba8b9717185ee30a Mon Sep 17 00:00:00 2001 From: gabokatta Date: Thu, 5 Sep 2024 03:19:06 -0300 Subject: [PATCH 027/103] added parenthesis support for future condition evaluation --- src/query/builder/select.rs | 16 +++++++-------- src/query/mod.rs | 2 ++ src/query/tokenizer.rs | 40 ++++++++++++++++++++++++++++++++++--- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/query/builder/select.rs b/src/query/builder/select.rs index 6fb2d5c..cf774a3 100644 --- a/src/query/builder/select.rs +++ b/src/query/builder/select.rs @@ -17,19 +17,19 @@ impl SelectBuilder { Self { tokens } } - fn process_table(&self) -> String { + fn parse_table(&self) -> String { "".to_string() } - fn process_fields(&self) -> Vec { + fn parse_fields(&self) -> Vec { vec![] } - fn process_ordering(&self) -> Ordering { + fn parse_ordering(&self) -> Ordering { Ordering::default() } - fn process_expressions(&self) -> Vec { + fn parse_statements(&self) -> Vec { vec![] } } @@ -40,10 +40,10 @@ impl Builder for SelectBuilder { self.validate_keywords()?; query.operation = Select; - query.table = self.process_table(); - query.fields = self.process_fields(); - query.expressions = self.process_expressions(); - query.ordering = self.process_ordering(); + query.table = self.parse_table(); + query.fields = self.parse_fields(); + query.expressions = self.parse_statements(); + query.ordering = self.parse_ordering(); Ok(query) } diff --git a/src/query/mod.rs b/src/query/mod.rs index fa1d704..f38604b 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -44,6 +44,8 @@ pub enum TokenKind { Number, Operator, Identifier, + ParenthesisOpen, + ParenthesisClose, Keyword, } diff --git a/src/query/tokenizer.rs b/src/query/tokenizer.rs index 21d21b4..feae1e2 100644 --- a/src/query/tokenizer.rs +++ b/src/query/tokenizer.rs @@ -2,12 +2,14 @@ use crate::errored; use crate::query::errors::InvalidSQL; use crate::query::errors::InvalidSQL::Syntax; use crate::query::tokenizer::TokenizerState::*; -use crate::query::TokenKind::{Identifier, Keyword, Number, Unknown}; +use crate::query::TokenKind::{ + Identifier, Keyword, Number, ParenthesisClose, ParenthesisOpen, Unknown, +}; use crate::query::{Token, TokenKind}; const VALID_OPERATORS: &[&str] = &["*", "=", "<", ">", ">=", "<=", "!=", "<>"]; -const IGNORABLE_CHARS: &[char] = &[' ', '(', ')', ',', ';', '\0', '\n']; +const IGNORABLE_CHARS: &[char] = &[' ', ',', ';', '\0', '\n']; const RESERVED_KEYWORDS: &[&str] = &[ "SELECT", @@ -29,6 +31,7 @@ const RESERVED_KEYWORDS: &[&str] = &[ pub struct Tokenizer { i: usize, state: TokenizerState, + parenthesis_count: u8, } enum TokenizerState { @@ -37,12 +40,18 @@ enum TokenizerState { Operator, NumberLiteral, StringLiteral, + OpenParenthesis, + CloseParenthesis, Complete, } impl Tokenizer { pub fn new() -> Self { - Self { i: 0, state: Begin } + Self { + i: 0, + state: Begin, + parenthesis_count: 0, + } } pub fn tokenize(&mut self, sql: &str) -> Result, InvalidSQL> { @@ -55,6 +64,7 @@ impl Tokenizer { IdentifierOrKeyword => token = self.tokenize_identifier_or_keyword(sql)?, Operator => token = self.tokenize_operator(sql)?, NumberLiteral => token = self.tokenize_number(sql)?, + OpenParenthesis | CloseParenthesis => token = self.tokenize_parenthesis(sql)?, StringLiteral => { self.i += c.len_utf8(); token = self.tokenize_quoted(sql)?; @@ -69,6 +79,9 @@ impl Tokenizer { if token.kind != Unknown { out.push(token); } + if self.parenthesis_count != 0 { + errored!(Syntax, "unclosed parentheses found.") + } Ok(out) } @@ -78,6 +91,8 @@ impl Tokenizer { c if c.is_ascii_digit() => self.state = NumberLiteral, c if is_identifier_char(c) => self.state = IdentifierOrKeyword, '\'' => self.state = StringLiteral, + '(' => self.state = OpenParenthesis, + ')' => self.state = CloseParenthesis, c if is_operator_char(c) => self.state = Operator, _ => errored!( Syntax, @@ -89,6 +104,25 @@ impl Tokenizer { Ok(()) } + fn tokenize_parenthesis(&mut self, sql: &str) -> Result { + let c = char_at(self.i, sql); + let mut token = Token::default(); + if c == '(' { + self.parenthesis_count += 1; + token.kind = ParenthesisOpen + } else if c == ')' { + self.parenthesis_count -= 1; + token.kind = ParenthesisClose + } else { + errored!(Syntax, "unrecognized token {} at char {}", c, self.i) + } + + self.i += c.len_utf8(); + self.state = Complete; + token.value = c.to_string(); + Ok(token) + } + fn tokenize_identifier_or_keyword(&mut self, sql: &str) -> Result { if let Some(word) = self.matches_keyword(sql) { self.i += word.len(); From c66d2d2f148837b8f6c5f97b48628715918239f5 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Thu, 5 Sep 2024 14:33:56 -0300 Subject: [PATCH 028/103] added table and fields parsing for select --- src/query/builder/select.rs | 68 ++++++++++++++++++++++++++++++++----- src/query/mod.rs | 12 +++++-- 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/src/query/builder/select.rs b/src/query/builder/select.rs index cf774a3..8a77eb2 100644 --- a/src/query/builder/select.rs +++ b/src/query/builder/select.rs @@ -1,7 +1,10 @@ +use crate::errored; use crate::query::builder::{validate_keywords, Builder}; use crate::query::errors::InvalidSQL; +use crate::query::errors::InvalidSQL::Syntax; use crate::query::Operation::Select; -use crate::query::{Ordering, Query, Statement, Token}; +use crate::query::TokenKind::{Identifier, Keyword, Operator}; +use crate::query::{Ordering, Query, Statement, Token, TokenKind}; use std::collections::VecDeque; const ALLOWED_KEYWORDS: &[&str] = &[ @@ -17,19 +20,66 @@ impl SelectBuilder { Self { tokens } } - fn parse_table(&self) -> String { - "".to_string() + fn parse_table(&mut self) -> Result { + if let Some(t) = self.tokens.front() { + if t.kind != Keyword || t.value != "FROM" { + errored!(Syntax, "missing FROM clause, got: {}", t.value) + } + self.tokens.pop_front(); + } + match self.tokens.pop_front() { + None => errored!(Syntax, "could not find table identifier."), + Some(t) => { + if t.kind != Identifier { + errored!( + Syntax, + "expected table identifier, found {} of kind {:?}", + t.value, + t.kind + ) + } + Ok(t.value) + } + } } - fn parse_fields(&self) -> Vec { - vec![] + fn parse_fields(&mut self) -> Result, InvalidSQL> { + let mut fields: Vec = vec![]; + while let Some(t) = self.tokens.front() { + match t.kind { + Identifier => { + if let Some(op) = self.tokens.pop_front() { + fields.push(op); + } + } + Operator => { + if t.value != "*" { + errored!(Syntax, "invalid operating in SELECT fields: {}", t.value) + } + if let Some(op) = self.tokens.pop_front() { + fields.push(op); + break; + } + } + Keyword if t.value == "FROM" => { + break; + } + _ => errored!( + Syntax, + "unexpected token while parsing SELECT fields: {} of kind {:?}", + t.value, + t.kind + ), + } + } + Ok(fields) } fn parse_ordering(&self) -> Ordering { Ordering::default() } - fn parse_statements(&self) -> Vec { + fn parse_conditions(&self) -> Vec { vec![] } } @@ -40,9 +90,9 @@ impl Builder for SelectBuilder { self.validate_keywords()?; query.operation = Select; - query.table = self.parse_table(); - query.fields = self.parse_fields(); - query.expressions = self.parse_statements(); + query.fields = self.parse_fields()?; + query.table = self.parse_table()?; + query.expressions = self.parse_conditions(); query.ordering = self.parse_ordering(); Ok(query) diff --git a/src/query/mod.rs b/src/query/mod.rs index f38604b..bb12a5c 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -23,8 +23,16 @@ pub struct Token { pub kind: TokenKind, } +pub enum Expression { + Condition(Statement), + Assignment(Statement), + Group(Vec), + And(Box, Box), + Or(Box, Box), +} + #[derive(Debug)] -struct Statement { +pub struct Statement { kind: StatementKind, operator: Token, left: Token, @@ -59,7 +67,7 @@ pub enum Operation { } #[derive(Debug, PartialEq)] -enum StatementKind { +pub enum StatementKind { Condition, Assignment, } From 7577dc917b08badbde3fcd2ec3d0fc90fbe9e649 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Thu, 5 Sep 2024 20:12:26 -0300 Subject: [PATCH 029/103] added order by parsing --- src/query/builder/mod.rs | 13 +++++- src/query/builder/select.rs | 84 ++++++++++++++++++++++++------------- src/query/builder/where.rs | 14 +++++++ src/query/mod.rs | 10 ++--- 4 files changed, 86 insertions(+), 35 deletions(-) create mode 100644 src/query/builder/where.rs diff --git a/src/query/builder/mod.rs b/src/query/builder/mod.rs index 893f58f..f07ff2c 100644 --- a/src/query/builder/mod.rs +++ b/src/query/builder/mod.rs @@ -2,6 +2,7 @@ mod delete; mod insert; mod select; mod update; +mod r#where; use crate::errored; use crate::query::builder::delete::DeleteBuilder; @@ -12,7 +13,7 @@ use crate::query::errors::InvalidSQL; use crate::query::errors::InvalidSQL::Syntax; use crate::query::Operation::{Delete, Insert, Select, Unknown, Update}; use crate::query::TokenKind::Keyword; -use crate::query::{Operation, Query, Token}; +use crate::query::{Operation, Query, Token, TokenKind}; use std::collections::VecDeque; pub trait Builder { @@ -65,3 +66,13 @@ fn validate_keywords( } Ok(()) } + +fn unexpected_token(stage: String, token: &Token) -> Result<(), InvalidSQL> { + errored!( + Syntax, + "unexpected token while parsing {} fields: {} of kind {:?}", + stage, + token.value, + token.kind + ) +} diff --git a/src/query/builder/select.rs b/src/query/builder/select.rs index 8a77eb2..fbc789c 100644 --- a/src/query/builder/select.rs +++ b/src/query/builder/select.rs @@ -1,23 +1,29 @@ use crate::errored; -use crate::query::builder::{validate_keywords, Builder}; +use crate::query::builder::r#where::WhereBuilder; +use crate::query::builder::{unexpected_token, validate_keywords, Builder}; use crate::query::errors::InvalidSQL; use crate::query::errors::InvalidSQL::Syntax; use crate::query::Operation::Select; -use crate::query::TokenKind::{Identifier, Keyword, Operator}; -use crate::query::{Ordering, Query, Statement, Token, TokenKind}; +use crate::query::OrderKind::{Asc, Desc}; +use crate::query::TokenKind::{Identifier, Keyword, Operator, Unknown}; +use crate::query::{OrderKind, Ordering, Query, Token}; use std::collections::VecDeque; const ALLOWED_KEYWORDS: &[&str] = &[ - "SELECT", "FROM", "WHERE", "ORDER_BY", "ASC", "DESC", "AND", "OR", + "SELECT", "FROM", "WHERE", "ORDER BY", "ASC", "DESC", "AND", "OR", ]; pub struct SelectBuilder { tokens: VecDeque, + where_builder: WhereBuilder, } impl SelectBuilder { pub fn new(tokens: VecDeque) -> Self { - Self { tokens } + Self { + tokens, + where_builder: WhereBuilder::new(), + } } fn parse_table(&mut self) -> Result { @@ -31,12 +37,7 @@ impl SelectBuilder { None => errored!(Syntax, "could not find table identifier."), Some(t) => { if t.kind != Identifier { - errored!( - Syntax, - "expected table identifier, found {} of kind {:?}", - t.value, - t.kind - ) + unexpected_token("TABLE".to_string(), &t)? } Ok(t.value) } @@ -52,10 +53,7 @@ impl SelectBuilder { fields.push(op); } } - Operator => { - if t.value != "*" { - errored!(Syntax, "invalid operating in SELECT fields: {}", t.value) - } + Operator if t.value == "*" => { if let Some(op) = self.tokens.pop_front() { fields.push(op); break; @@ -64,23 +62,43 @@ impl SelectBuilder { Keyword if t.value == "FROM" => { break; } - _ => errored!( - Syntax, - "unexpected token while parsing SELECT fields: {} of kind {:?}", - t.value, - t.kind - ), + _ => unexpected_token("SELECT".to_string(), t)?, } } Ok(fields) } - fn parse_ordering(&self) -> Ordering { - Ordering::default() - } - - fn parse_conditions(&self) -> Vec { - vec![] + fn parse_ordering(&mut self) -> Result, InvalidSQL> { + let mut ordering = vec![]; + if let Some(t) = self.tokens.front() { + if t.kind != Keyword || t.value != "ORDER BY" { + errored!(Syntax, "missing ORDER BY clause, got: {}", t.value) + } + self.tokens.pop_front(); + } else { + return Ok(ordering); + } + while let Some(t) = self.tokens.front() { + if t.kind != Identifier { + unexpected_token("ORDER_BY fields".to_string(), t)? + } else if let Some(i) = self.tokens.pop_front() { + let mut new_order = Ordering::default(); + new_order.field = i; + ordering.push(new_order) + } + if let Some(next) = self.tokens.front() { + let mut ordering_kind: Option = None; + match next.kind { + Keyword if next.value == "DESC" => ordering_kind = Some(Desc), + _ => {} + } + self.tokens.pop_front(); + if let Some(o) = ordering.last_mut() { + o.kind = ordering_kind.unwrap_or(Asc); + } + } + } + Ok(ordering) } } @@ -92,8 +110,8 @@ impl Builder for SelectBuilder { query.operation = Select; query.fields = self.parse_fields()?; query.table = self.parse_table()?; - query.expressions = self.parse_conditions(); - query.ordering = self.parse_ordering(); + query.expressions = self.where_builder.parse_conditions()?; + query.ordering = self.parse_ordering()?; Ok(query) } @@ -102,3 +120,11 @@ impl Builder for SelectBuilder { validate_keywords(ALLOWED_KEYWORDS, &self.tokens, Select) } } + +fn match_ordering(token: &Token) -> Result { + match token.value.as_str() { + "ASC" => Ok(Asc), + "DESC" => Ok(Desc), + _ => errored!(Syntax, "expected ordering operator, got: {}", token.value), + } +} diff --git a/src/query/builder/where.rs b/src/query/builder/where.rs new file mode 100644 index 0000000..4a92195 --- /dev/null +++ b/src/query/builder/where.rs @@ -0,0 +1,14 @@ +use crate::query::errors::InvalidSQL; +use crate::query::Statement; + +pub struct WhereBuilder; + +impl WhereBuilder { + pub fn new() -> Self { + Self + } + + pub fn parse_conditions(&self) -> Result, InvalidSQL> { + Ok(vec![]) + } +} diff --git a/src/query/mod.rs b/src/query/mod.rs index bb12a5c..2d34295 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -6,7 +6,7 @@ pub mod tokenizer; use crate::errors::Errored; use crate::query::OrderKind::Asc; use crate::query::StatementKind::Condition; -use crate::query::TokenKind::Unknown; +use crate::query::TokenKind::{Identifier, Unknown}; #[derive(Debug)] pub struct Query { @@ -14,7 +14,7 @@ pub struct Query { pub table: String, fields: Vec, expressions: Vec, - ordering: Ordering, + ordering: Vec, } #[derive(Debug)] @@ -41,7 +41,7 @@ pub struct Statement { #[derive(Debug)] struct Ordering { - fields: Vec, + field: Token, kind: OrderKind, } @@ -81,7 +81,7 @@ enum OrderKind { impl Ordering { pub fn default() -> Self { Self { - fields: vec![], + field: Token::default(), kind: Asc, } } @@ -114,7 +114,7 @@ impl Query { table: "".to_string(), fields: vec![], expressions: vec![], - ordering: Ordering::default(), + ordering: vec![], } } } From 7e2e64b6b906dd7cfbfe84f67b906680fb2a8f12 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Thu, 5 Sep 2024 20:25:41 -0300 Subject: [PATCH 030/103] improved query debugging logs --- src/query/builder/select.rs | 2 +- src/query/mod.rs | 28 +++++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/query/builder/select.rs b/src/query/builder/select.rs index fbc789c..f41e1f8 100644 --- a/src/query/builder/select.rs +++ b/src/query/builder/select.rs @@ -80,7 +80,7 @@ impl SelectBuilder { } while let Some(t) = self.tokens.front() { if t.kind != Identifier { - unexpected_token("ORDER_BY fields".to_string(), t)? + unexpected_token("ORDER_BY".to_string(), t)? } else if let Some(i) = self.tokens.pop_front() { let mut new_order = Ordering::default(); new_order.field = i; diff --git a/src/query/mod.rs b/src/query/mod.rs index 2d34295..ae05ad2 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -6,9 +6,9 @@ pub mod tokenizer; use crate::errors::Errored; use crate::query::OrderKind::Asc; use crate::query::StatementKind::Condition; -use crate::query::TokenKind::{Identifier, Unknown}; +use crate::query::TokenKind::Unknown; +use std::fmt::{Debug, Display, Formatter}; -#[derive(Debug)] pub struct Query { pub operation: Operation, pub table: String, @@ -39,7 +39,6 @@ pub struct Statement { right: Token, } -#[derive(Debug)] struct Ordering { field: Token, kind: OrderKind, @@ -119,6 +118,29 @@ impl Query { } } +impl Display for Query { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Query Kind: [{:?}]", self.operation)?; + writeln!(f, "Table: {:?}", self.table)?; + let fields: Vec<&str> = self.fields.iter().map(|f| f.value.as_str()).collect(); + writeln!(f, "Fields: {:?}", fields)?; + writeln!(f, "Expressions: {:?}", self.expressions)?; + writeln!(f, "Ordering: {:?}", self.ordering) + } +} + +impl Debug for Query { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self) + } +} + +impl Debug for Ordering { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "({}:{:?})", &self.field.value, &self.kind) + } +} + pub fn validate_query_string(query: &str) -> Result<(), Errored> { if query.trim().is_empty() { return Err(Errored(String::from("query is empty."))); From 9b352cbad5b79a1cdde223fdf593f3dabcc0e27a Mon Sep 17 00:00:00 2001 From: gabokatta Date: Thu, 5 Sep 2024 23:14:12 -0300 Subject: [PATCH 031/103] buggy where parser --- src/query/builder/expression.rs | 107 ++++++++++++++++++++++++++++++++ src/query/builder/mod.rs | 6 +- src/query/builder/select.rs | 66 ++++++++++---------- src/query/builder/where.rs | 14 ----- src/query/mod.rs | 55 ++++++---------- src/query/tokenizer.rs | 2 +- 6 files changed, 163 insertions(+), 87 deletions(-) create mode 100644 src/query/builder/expression.rs delete mode 100644 src/query/builder/where.rs diff --git a/src/query/builder/expression.rs b/src/query/builder/expression.rs new file mode 100644 index 0000000..f592d92 --- /dev/null +++ b/src/query/builder/expression.rs @@ -0,0 +1,107 @@ +use crate::errored; +use crate::query::errors::InvalidSQL; +use crate::query::errors::InvalidSQL::Syntax; +use crate::query::TokenKind::ParenthesisClose; +use crate::query::{Token, TokenKind}; +use std::collections::VecDeque; + +pub struct ExpressionBuilder; + +#[derive(Debug, Default, PartialEq)] +pub enum ExpressionOperator { + #[default] + None, + Equals, + NotEquals, + GreaterThan, + LessThan, + GreaterOrEqual, + LessOrEqual, + And, + Or, + Not, +} + +#[derive(Default)] +pub enum ExpressionNode { + #[default] + Empty, + Leaf(Token), + Statement { + operator: ExpressionOperator, + left: Box, + right: Box, + }, +} + +impl ExpressionBuilder { + pub fn new() -> Self { + Self + } + + pub fn parse_expressions( + &self, + tokens: &mut VecDeque, + ) -> Result { + self.parse_statement(tokens) + } + + fn parse_leaf(&self, tokens: &mut VecDeque) -> Result { + if let Some(t) = tokens.pop_front() { + match t.kind { + TokenKind::String | TokenKind::Number | TokenKind::Identifier => { + Ok(ExpressionNode::Leaf(t)) + } + TokenKind::ParenthesisOpen => { + let node = self.parse_expressions(tokens)?; + if let Some(next) = tokens.front() { + if next.kind != ParenthesisClose { + errored!(Syntax, "expected closing parenthesis, got: {:?}", t) + } + } + Ok(node) + } + _ => errored!(Syntax, "invalid token during WHERE parsing: {:?}", t), + } + } else { + errored!(Syntax, "missing tokens while parsing conditions.") + } + } + + fn parse_statement(&self, tokens: &mut VecDeque) -> Result { + let mut left = self.parse_leaf(tokens)?; + while let Some(t) = tokens.front() { + let operator: ExpressionOperator = match t.kind { + TokenKind::Operator => match t.value.as_str() { + ">" => ExpressionOperator::GreaterThan, + "=" => ExpressionOperator::Equals, + "<" => ExpressionOperator::LessThan, + ">=" => ExpressionOperator::GreaterOrEqual, + "<=" => ExpressionOperator::LessOrEqual, + "!=" => ExpressionOperator::NotEquals, + _ => ExpressionOperator::None, + }, + TokenKind::Keyword => match t.value.as_str() { + "AND" => ExpressionOperator::And, + "OR" => ExpressionOperator::Or, + "NOT" => ExpressionOperator::Not, + _ => ExpressionOperator::None, + }, + _ => ExpressionOperator::None, + }; + if operator == ExpressionOperator::None { + break; + } + tokens.pop_front(); + + let right = self.parse_leaf(tokens)?; + left = ExpressionNode::Statement { + operator, + left: Box::new(left), + right: Box::new(right), + } + } + + Ok(left) + } +} diff --git a/src/query/builder/mod.rs b/src/query/builder/mod.rs index f07ff2c..73aa3c6 100644 --- a/src/query/builder/mod.rs +++ b/src/query/builder/mod.rs @@ -1,8 +1,8 @@ mod delete; +pub mod expression; mod insert; mod select; mod update; -mod r#where; use crate::errored; use crate::query::builder::delete::DeleteBuilder; @@ -13,7 +13,7 @@ use crate::query::errors::InvalidSQL; use crate::query::errors::InvalidSQL::Syntax; use crate::query::Operation::{Delete, Insert, Select, Unknown, Update}; use crate::query::TokenKind::Keyword; -use crate::query::{Operation, Query, Token, TokenKind}; +use crate::query::{Operation, Query, Token}; use std::collections::VecDeque; pub trait Builder { @@ -67,7 +67,7 @@ fn validate_keywords( Ok(()) } -fn unexpected_token(stage: String, token: &Token) -> Result<(), InvalidSQL> { +fn unexpected_token_in_stage(stage: String, token: &Token) -> Result<(), InvalidSQL> { errored!( Syntax, "unexpected token while parsing {} fields: {} of kind {:?}", diff --git a/src/query/builder/select.rs b/src/query/builder/select.rs index f41e1f8..82be20c 100644 --- a/src/query/builder/select.rs +++ b/src/query/builder/select.rs @@ -1,12 +1,12 @@ use crate::errored; -use crate::query::builder::r#where::WhereBuilder; -use crate::query::builder::{unexpected_token, validate_keywords, Builder}; +use crate::query::builder::expression::{ExpressionBuilder, ExpressionNode}; +use crate::query::builder::{unexpected_token_in_stage, validate_keywords, Builder}; use crate::query::errors::InvalidSQL; use crate::query::errors::InvalidSQL::Syntax; use crate::query::Operation::Select; use crate::query::OrderKind::{Asc, Desc}; -use crate::query::TokenKind::{Identifier, Keyword, Operator, Unknown}; -use crate::query::{OrderKind, Ordering, Query, Token}; +use crate::query::TokenKind::{Identifier, Keyword, Operator}; +use crate::query::{Ordering, Query, Token}; use std::collections::VecDeque; const ALLOWED_KEYWORDS: &[&str] = &[ @@ -15,14 +15,14 @@ const ALLOWED_KEYWORDS: &[&str] = &[ pub struct SelectBuilder { tokens: VecDeque, - where_builder: WhereBuilder, + where_builder: ExpressionBuilder, } impl SelectBuilder { pub fn new(tokens: VecDeque) -> Self { Self { tokens, - where_builder: WhereBuilder::new(), + where_builder: ExpressionBuilder::new(), } } @@ -37,7 +37,7 @@ impl SelectBuilder { None => errored!(Syntax, "could not find table identifier."), Some(t) => { if t.kind != Identifier { - unexpected_token("TABLE".to_string(), &t)? + unexpected_token_in_stage("TABLE".to_string(), &t)? } Ok(t.value) } @@ -62,7 +62,7 @@ impl SelectBuilder { Keyword if t.value == "FROM" => { break; } - _ => unexpected_token("SELECT".to_string(), t)?, + _ => unexpected_token_in_stage("SELECT".to_string(), t)?, } } Ok(fields) @@ -70,36 +70,44 @@ impl SelectBuilder { fn parse_ordering(&mut self) -> Result, InvalidSQL> { let mut ordering = vec![]; - if let Some(t) = self.tokens.front() { - if t.kind != Keyword || t.value != "ORDER BY" { - errored!(Syntax, "missing ORDER BY clause, got: {}", t.value) - } + if self + .tokens + .front() + .map_or(false, |t| t.kind == Keyword && t.value == "ORDER BY") + { self.tokens.pop_front(); } else { return Ok(ordering); } - while let Some(t) = self.tokens.front() { + while let Some(t) = self.tokens.pop_front() { if t.kind != Identifier { - unexpected_token("ORDER_BY".to_string(), t)? - } else if let Some(i) = self.tokens.pop_front() { - let mut new_order = Ordering::default(); - new_order.field = i; - ordering.push(new_order) + unexpected_token_in_stage("ORDER_BY".to_string(), &t)? } + let mut new_order = Ordering::default(); + new_order.field = t; if let Some(next) = self.tokens.front() { - let mut ordering_kind: Option = None; match next.kind { - Keyword if next.value == "DESC" => ordering_kind = Some(Desc), + Keyword if next.value == "ASC" || next.value == "DESC" => { + new_order.kind = if next.value == "DESC" { Desc } else { Asc }; + self.tokens.pop_front(); + } _ => {} } - self.tokens.pop_front(); - if let Some(o) = ordering.last_mut() { - o.kind = ordering_kind.unwrap_or(Asc); - } } + ordering.push(new_order) } Ok(ordering) } + + fn parse_conditions(&mut self) -> Result { + if let Some(t) = self.tokens.front() { + if t.kind != Keyword || t.value != "WHERE" { + errored!(Syntax, "missing WHERE clause, got: {}", t.value) + } + self.tokens.pop_front(); + } + self.where_builder.parse_expressions(&mut self.tokens) + } } impl Builder for SelectBuilder { @@ -110,7 +118,7 @@ impl Builder for SelectBuilder { query.operation = Select; query.fields = self.parse_fields()?; query.table = self.parse_table()?; - query.expressions = self.where_builder.parse_conditions()?; + query.expressions = self.parse_conditions()?; query.ordering = self.parse_ordering()?; Ok(query) @@ -120,11 +128,3 @@ impl Builder for SelectBuilder { validate_keywords(ALLOWED_KEYWORDS, &self.tokens, Select) } } - -fn match_ordering(token: &Token) -> Result { - match token.value.as_str() { - "ASC" => Ok(Asc), - "DESC" => Ok(Desc), - _ => errored!(Syntax, "expected ordering operator, got: {}", token.value), - } -} diff --git a/src/query/builder/where.rs b/src/query/builder/where.rs deleted file mode 100644 index 4a92195..0000000 --- a/src/query/builder/where.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::query::errors::InvalidSQL; -use crate::query::Statement; - -pub struct WhereBuilder; - -impl WhereBuilder { - pub fn new() -> Self { - Self - } - - pub fn parse_conditions(&self) -> Result, InvalidSQL> { - Ok(vec![]) - } -} diff --git a/src/query/mod.rs b/src/query/mod.rs index ae05ad2..865e000 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -4,8 +4,8 @@ mod executor; pub mod tokenizer; use crate::errors::Errored; +use crate::query::builder::expression::ExpressionNode; use crate::query::OrderKind::Asc; -use crate::query::StatementKind::Condition; use crate::query::TokenKind::Unknown; use std::fmt::{Debug, Display, Formatter}; @@ -13,7 +13,7 @@ pub struct Query { pub operation: Operation, pub table: String, fields: Vec, - expressions: Vec, + expressions: ExpressionNode, ordering: Vec, } @@ -23,22 +23,6 @@ pub struct Token { pub kind: TokenKind, } -pub enum Expression { - Condition(Statement), - Assignment(Statement), - Group(Vec), - And(Box, Box), - Or(Box, Box), -} - -#[derive(Debug)] -pub struct Statement { - kind: StatementKind, - operator: Token, - left: Token, - right: Token, -} - struct Ordering { field: Token, kind: OrderKind, @@ -65,12 +49,6 @@ pub enum Operation { Insert, } -#[derive(Debug, PartialEq)] -pub enum StatementKind { - Condition, - Assignment, -} - #[derive(Debug)] enum OrderKind { Asc, @@ -86,17 +64,6 @@ impl Ordering { } } -impl Statement { - pub fn default() -> Self { - Self { - kind: Condition, - operator: Token::default(), - left: Token::default(), - right: Token::default(), - } - } -} - impl Token { pub fn default() -> Self { Self { @@ -112,7 +79,7 @@ impl Query { operation: Operation::Unknown, table: "".to_string(), fields: vec![], - expressions: vec![], + expressions: ExpressionNode::default(), ordering: vec![], } } @@ -141,6 +108,22 @@ impl Debug for Ordering { } } +impl Debug for ExpressionNode { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ExpressionNode::Empty => write!(f, "()"), + ExpressionNode::Leaf(t) => write!(f, "{}", t.value), + ExpressionNode::Statement { + operator, + left, + right, + } => { + write!(f, "[{:?}] ({:?},{:?})", operator, left, right) + } + } + } +} + pub fn validate_query_string(query: &str) -> Result<(), Errored> { if query.trim().is_empty() { return Err(Errored(String::from("query is empty."))); diff --git a/src/query/tokenizer.rs b/src/query/tokenizer.rs index feae1e2..08b2009 100644 --- a/src/query/tokenizer.rs +++ b/src/query/tokenizer.rs @@ -7,7 +7,7 @@ use crate::query::TokenKind::{ }; use crate::query::{Token, TokenKind}; -const VALID_OPERATORS: &[&str] = &["*", "=", "<", ">", ">=", "<=", "!=", "<>"]; +const VALID_OPERATORS: &[&str] = &["*", "=", "<", ">", "!", ">=", "<=", "!="]; const IGNORABLE_CHARS: &[char] = &[' ', ',', ';', '\0', '\n']; From 7aa07bc5f4f782d1a79783f69f0cddf896c689f8 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Fri, 6 Sep 2024 00:07:52 -0300 Subject: [PATCH 032/103] new draft of where parser using recursive descent parser --- src/query/builder/expression.rs | 150 +++++++++++++++++++++++--------- 1 file changed, 109 insertions(+), 41 deletions(-) diff --git a/src/query/builder/expression.rs b/src/query/builder/expression.rs index f592d92..1eb1401 100644 --- a/src/query/builder/expression.rs +++ b/src/query/builder/expression.rs @@ -1,7 +1,11 @@ use crate::errored; +use crate::query::builder::expression::ExpressionNode::Leaf; +use crate::query::builder::expression::ExpressionOperator::{ + Equals, GreaterOrEqual, GreaterThan, LessOrEqual, LessThan, NotEquals, +}; use crate::query::errors::InvalidSQL; use crate::query::errors::InvalidSQL::Syntax; -use crate::query::TokenKind::ParenthesisClose; +use crate::query::TokenKind::{Keyword, ParenthesisClose}; use crate::query::{Token, TokenKind}; use std::collections::VecDeque; @@ -43,65 +47,129 @@ impl ExpressionBuilder { &self, tokens: &mut VecDeque, ) -> Result { - self.parse_statement(tokens) + self.parse_or(tokens) } fn parse_leaf(&self, tokens: &mut VecDeque) -> Result { - if let Some(t) = tokens.pop_front() { + if let Some(t) = tokens.front() { match t.kind { - TokenKind::String | TokenKind::Number | TokenKind::Identifier => { - Ok(ExpressionNode::Leaf(t)) - } TokenKind::ParenthesisOpen => { - let node = self.parse_expressions(tokens)?; - if let Some(next) = tokens.front() { - if next.kind != ParenthesisClose { - errored!(Syntax, "expected closing parenthesis, got: {:?}", t) + tokens.pop_front(); + let expression = self.parse_expressions(tokens)?; + if let Some(t) = tokens.front() { + if t.kind == ParenthesisClose { + tokens.pop_front(); + Ok(expression) + } else { + errored!(Syntax, "unclosed parenthesis while evaluating WHERE.") } + } else { + errored!(Syntax, "") + } + } + TokenKind::Identifier | TokenKind::Number | TokenKind::String => { + if let Some(t) = tokens.pop_front() { + Ok(Leaf(t)) + } else { + errored!(Syntax, "") } - Ok(node) } - _ => errored!(Syntax, "invalid token during WHERE parsing: {:?}", t), + _ => { + errored!( + Syntax, + "unrecognized token while parsing comparison: {:?}.", + t + ) + } } } else { - errored!(Syntax, "missing tokens while parsing conditions.") + errored!(Syntax, "reached end of query while parsing comparisons.") } } - fn parse_statement(&self, tokens: &mut VecDeque) -> Result { - let mut left = self.parse_leaf(tokens)?; - while let Some(t) = tokens.front() { - let operator: ExpressionOperator = match t.kind { - TokenKind::Operator => match t.value.as_str() { - ">" => ExpressionOperator::GreaterThan, - "=" => ExpressionOperator::Equals, - "<" => ExpressionOperator::LessThan, - ">=" => ExpressionOperator::GreaterOrEqual, - "<=" => ExpressionOperator::LessOrEqual, - "!=" => ExpressionOperator::NotEquals, - _ => ExpressionOperator::None, - }, - TokenKind::Keyword => match t.value.as_str() { - "AND" => ExpressionOperator::And, - "OR" => ExpressionOperator::Or, - "NOT" => ExpressionOperator::Not, - _ => ExpressionOperator::None, - }, - _ => ExpressionOperator::None, + fn parse_simple_operator( + &self, + tokens: &mut VecDeque, + ) -> Result { + if let Some(t) = tokens.front() { + let op = match t.value.as_str() { + "=" => Equals, + "!=" => NotEquals, + ">" => GreaterThan, + ">=" => GreaterOrEqual, + "<" => LessThan, + "<=" => LessOrEqual, + _ => errored!(Syntax, "invalid operator, got: {}", t.value), }; - if operator == ExpressionOperator::None { - break; - } tokens.pop_front(); + Ok(op) + } else { + errored!(Syntax, "expected operator but was end of query.") + } + } - let right = self.parse_leaf(tokens)?; - left = ExpressionNode::Statement { - operator, - left: Box::new(left), - right: Box::new(right), + fn parse_comparisons( + &self, + tokens: &mut VecDeque, + ) -> Result { + let left = self.parse_leaf(tokens)?; + let operator = self.parse_simple_operator(tokens)?; + let right = self.parse_leaf(tokens)?; + Ok(ExpressionNode::Statement { + operator, + left: Box::new(left), + right: Box::new(right), + }) + } + + fn parse_and(&self, tokens: &mut VecDeque) -> Result { + let mut left = self.parse_not(tokens)?; + while let Some(t) = tokens.front() { + if t.kind == Keyword && t.value == "AND" { + tokens.pop_front(); // Consume 'AND' + let right = self.parse_not(tokens)?; + left = ExpressionNode::Statement { + operator: ExpressionOperator::And, + left: Box::new(left), + right: Box::new(right), + }; + } else { + break; } } + Ok(left) + } + fn parse_or(&self, tokens: &mut VecDeque) -> Result { + let mut left = self.parse_and(tokens)?; + while let Some(t) = tokens.front() { + if t.kind == Keyword && t.value == "OR" { + tokens.pop_front(); // Consume 'OR' + let right = self.parse_and(tokens)?; + left = ExpressionNode::Statement { + operator: ExpressionOperator::Or, + left: Box::new(left), + right: Box::new(right), + }; + } else { + break; + } + } Ok(left) } + + fn parse_not(&self, tokens: &mut VecDeque) -> Result { + if let Some(t) = tokens.front() { + if t.kind == Keyword && t.value == "NOT" { + tokens.pop_front(); // Consume 'NOT' + let node = self.parse_leaf(tokens)?; + return Ok(ExpressionNode::Statement { + operator: ExpressionOperator::Not, + left: Box::new(node), + right: Box::new(ExpressionNode::Empty), + }); + } + } + self.parse_comparisons(tokens) + } } From efec6078fb3c6831c41a232da9e3d629169304d4 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Fri, 6 Sep 2024 00:41:28 -0300 Subject: [PATCH 033/103] refactored ExpressionBuilder to reduce ugly indentation --- src/query/builder/expression.rs | 188 ++++++++++++++++---------------- src/query/builder/select.rs | 2 +- 2 files changed, 93 insertions(+), 97 deletions(-) diff --git a/src/query/builder/expression.rs b/src/query/builder/expression.rs index 1eb1401..933d3da 100644 --- a/src/query/builder/expression.rs +++ b/src/query/builder/expression.rs @@ -50,110 +50,36 @@ impl ExpressionBuilder { self.parse_or(tokens) } - fn parse_leaf(&self, tokens: &mut VecDeque) -> Result { - if let Some(t) = tokens.front() { - match t.kind { - TokenKind::ParenthesisOpen => { - tokens.pop_front(); - let expression = self.parse_expressions(tokens)?; - if let Some(t) = tokens.front() { - if t.kind == ParenthesisClose { - tokens.pop_front(); - Ok(expression) - } else { - errored!(Syntax, "unclosed parenthesis while evaluating WHERE.") - } - } else { - errored!(Syntax, "") - } - } - TokenKind::Identifier | TokenKind::Number | TokenKind::String => { - if let Some(t) = tokens.pop_front() { - Ok(Leaf(t)) - } else { - errored!(Syntax, "") - } - } - _ => { - errored!( - Syntax, - "unrecognized token while parsing comparison: {:?}.", - t - ) - } - } - } else { - errored!(Syntax, "reached end of query while parsing comparisons.") - } - } - - fn parse_simple_operator( - &self, - tokens: &mut VecDeque, - ) -> Result { - if let Some(t) = tokens.front() { - let op = match t.value.as_str() { - "=" => Equals, - "!=" => NotEquals, - ">" => GreaterThan, - ">=" => GreaterOrEqual, - "<" => LessThan, - "<=" => LessOrEqual, - _ => errored!(Syntax, "invalid operator, got: {}", t.value), - }; - tokens.pop_front(); - Ok(op) - } else { - errored!(Syntax, "expected operator but was end of query.") - } - } - - fn parse_comparisons( - &self, - tokens: &mut VecDeque, - ) -> Result { - let left = self.parse_leaf(tokens)?; - let operator = self.parse_simple_operator(tokens)?; - let right = self.parse_leaf(tokens)?; - Ok(ExpressionNode::Statement { - operator, - left: Box::new(left), - right: Box::new(right), - }) - } - - fn parse_and(&self, tokens: &mut VecDeque) -> Result { - let mut left = self.parse_not(tokens)?; + fn parse_or(&self, tokens: &mut VecDeque) -> Result { + let mut left = self.parse_and(tokens)?; while let Some(t) = tokens.front() { - if t.kind == Keyword && t.value == "AND" { - tokens.pop_front(); // Consume 'AND' - let right = self.parse_not(tokens)?; - left = ExpressionNode::Statement { - operator: ExpressionOperator::And, - left: Box::new(left), - right: Box::new(right), - }; - } else { + if t.kind != Keyword || t.value != "OR" { break; } + tokens.pop_front(); + let right = self.parse_and(tokens)?; + left = ExpressionNode::Statement { + operator: ExpressionOperator::Or, + left: Box::new(left), + right: Box::new(right), + }; } Ok(left) } - fn parse_or(&self, tokens: &mut VecDeque) -> Result { - let mut left = self.parse_and(tokens)?; + fn parse_and(&self, tokens: &mut VecDeque) -> Result { + let mut left = self.parse_not(tokens)?; while let Some(t) = tokens.front() { - if t.kind == Keyword && t.value == "OR" { - tokens.pop_front(); // Consume 'OR' - let right = self.parse_and(tokens)?; - left = ExpressionNode::Statement { - operator: ExpressionOperator::Or, - left: Box::new(left), - right: Box::new(right), - }; - } else { + if t.kind != Keyword || t.value != "AND" { break; } + tokens.pop_front(); + let right = self.parse_not(tokens)?; + left = ExpressionNode::Statement { + operator: ExpressionOperator::And, + left: Box::new(left), + right: Box::new(right), + }; } Ok(left) } @@ -161,8 +87,8 @@ impl ExpressionBuilder { fn parse_not(&self, tokens: &mut VecDeque) -> Result { if let Some(t) = tokens.front() { if t.kind == Keyword && t.value == "NOT" { - tokens.pop_front(); // Consume 'NOT' - let node = self.parse_leaf(tokens)?; + tokens.pop_front(); + let node = self.parse_comparisons(tokens)?; return Ok(ExpressionNode::Statement { operator: ExpressionOperator::Not, left: Box::new(node), @@ -172,4 +98,74 @@ impl ExpressionBuilder { } self.parse_comparisons(tokens) } + + fn parse_leaf(&self, tokens: &mut VecDeque) -> Result { + let t = tokens + .front() + .ok_or_else(|| Syntax("reached end of query while parsing comparisons.".to_string()))?; + + match t.kind { + TokenKind::ParenthesisOpen => { + tokens.pop_front(); + let expression = self.parse_expressions(tokens)?; + let t = tokens.front().ok_or_else(|| { + Syntax("unclosed parenthesis while evaluating WHERE.".to_string()) + })?; + + if t.kind != ParenthesisClose { + errored!(Syntax, "unclosed parenthesis while evaluating WHERE."); + } + + tokens.pop_front(); + Ok(expression) + } + TokenKind::Identifier | TokenKind::Number | TokenKind::String => { + Ok(Leaf(tokens.pop_front().unwrap())) + } + _ => { + errored!( + Syntax, + "unrecognized token while parsing comparison: {:?}.", + t + ) + } + } + } + + fn parse_simple_operator( + &self, + tokens: &mut VecDeque, + ) -> Result { + let t = tokens + .front() + .ok_or_else(|| Syntax("expected operator but was end of query.".to_string()))?; + let op = match t.value.as_str() { + "=" => Equals, + "!=" => NotEquals, + ">" => GreaterThan, + ">=" => GreaterOrEqual, + "<" => LessThan, + "<=" => LessOrEqual, + _ => errored!(Syntax, "invalid operator, got: {}", t.value), + }; + tokens.pop_front(); + Ok(op) + } + + fn parse_comparisons( + &self, + tokens: &mut VecDeque, + ) -> Result { + let left = self.parse_leaf(tokens)?; + let operator = self.parse_simple_operator(tokens); + if operator.is_err() { + return Ok(left); + } + let right = self.parse_leaf(tokens)?; + Ok(ExpressionNode::Statement { + operator: operator?, + left: Box::new(left), + right: Box::new(right), + }) + } } diff --git a/src/query/builder/select.rs b/src/query/builder/select.rs index 82be20c..267223f 100644 --- a/src/query/builder/select.rs +++ b/src/query/builder/select.rs @@ -10,7 +10,7 @@ use crate::query::{Ordering, Query, Token}; use std::collections::VecDeque; const ALLOWED_KEYWORDS: &[&str] = &[ - "SELECT", "FROM", "WHERE", "ORDER BY", "ASC", "DESC", "AND", "OR", + "SELECT", "FROM", "WHERE", "ORDER BY", "ASC", "DESC", "AND", "OR", "NOT", ]; pub struct SelectBuilder { From c1852c20abbd2bc48d49c081bf654e0db2acc919 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Fri, 6 Sep 2024 00:42:18 -0300 Subject: [PATCH 034/103] keeping functions below 30 linebreaks --- src/query/builder/expression.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/query/builder/expression.rs b/src/query/builder/expression.rs index 933d3da..eee9e93 100644 --- a/src/query/builder/expression.rs +++ b/src/query/builder/expression.rs @@ -103,7 +103,6 @@ impl ExpressionBuilder { let t = tokens .front() .ok_or_else(|| Syntax("reached end of query while parsing comparisons.".to_string()))?; - match t.kind { TokenKind::ParenthesisOpen => { tokens.pop_front(); @@ -111,11 +110,9 @@ impl ExpressionBuilder { let t = tokens.front().ok_or_else(|| { Syntax("unclosed parenthesis while evaluating WHERE.".to_string()) })?; - if t.kind != ParenthesisClose { errored!(Syntax, "unclosed parenthesis while evaluating WHERE."); } - tokens.pop_front(); Ok(expression) } From ae523e7c52a39b49e4b034ffc0e1007aedca80a6 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Fri, 6 Sep 2024 01:07:14 -0300 Subject: [PATCH 035/103] more refactors --- src/query/builder/expression.rs | 31 +++++++++++++++---------------- src/query/mod.rs | 2 +- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/query/builder/expression.rs b/src/query/builder/expression.rs index eee9e93..c310a40 100644 --- a/src/query/builder/expression.rs +++ b/src/query/builder/expression.rs @@ -107,25 +107,24 @@ impl ExpressionBuilder { TokenKind::ParenthesisOpen => { tokens.pop_front(); let expression = self.parse_expressions(tokens)?; - let t = tokens.front().ok_or_else(|| { - Syntax("unclosed parenthesis while evaluating WHERE.".to_string()) - })?; - if t.kind != ParenthesisClose { - errored!(Syntax, "unclosed parenthesis while evaluating WHERE."); - } + tokens + .front() + .filter(|t| t.kind == ParenthesisClose) + .ok_or_else(|| { + Syntax("unclosed parenthesis while evaluating WHERE.".to_string()) + })?; tokens.pop_front(); Ok(expression) } - TokenKind::Identifier | TokenKind::Number | TokenKind::String => { - Ok(Leaf(tokens.pop_front().unwrap())) - } - _ => { - errored!( - Syntax, - "unrecognized token while parsing comparison: {:?}.", - t - ) - } + TokenKind::Identifier | TokenKind::Number | TokenKind::String => tokens + .pop_front() + .map(Leaf) + .ok_or_else(|| Syntax("failed to parse leaf node".to_string())), + _ => errored!( + Syntax, + "unrecognized token while parsing comparison: {:?}.", + t + ), } } diff --git a/src/query/mod.rs b/src/query/mod.rs index 865e000..6217b88 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -118,7 +118,7 @@ impl Debug for ExpressionNode { left, right, } => { - write!(f, "[{:?}] ({:?},{:?})", operator, left, right) + write!(f, "{:?}[{:?},{:?}]", operator, left, right) } } } From 57cdad1644a6d865fbe1672d38376d6168648b0d Mon Sep 17 00:00:00 2001 From: gabokatta Date: Fri, 6 Sep 2024 20:26:54 -0300 Subject: [PATCH 036/103] finished implementing rest of query builders. --- src/query/builder/delete.rs | 12 ++++- src/query/builder/expression.rs | 65 +++++++++++-------------- src/query/builder/insert.rs | 41 +++++++++++++++- src/query/builder/mod.rs | 86 ++++++++++++++++++++++++++++++++- src/query/builder/select.rs | 79 ++++-------------------------- src/query/builder/update.rs | 35 +++++++++++++- src/query/mod.rs | 21 +++++--- 7 files changed, 217 insertions(+), 122 deletions(-) diff --git a/src/query/builder/delete.rs b/src/query/builder/delete.rs index 2f56c52..c209574 100644 --- a/src/query/builder/delete.rs +++ b/src/query/builder/delete.rs @@ -1,4 +1,4 @@ -use crate::query::builder::{validate_keywords, Builder}; +use crate::query::builder::{unexpected_token_in_stage, validate_keywords, Builder}; use crate::query::errors::InvalidSQL; use crate::query::Operation::Delete; use crate::query::{Query, Token}; @@ -19,11 +19,19 @@ impl DeleteBuilder { impl Builder for DeleteBuilder { fn build(&mut self) -> Result { let mut query = Query::default(); - query.operation = Delete; self.validate_keywords()?; + query.operation = Delete; + query.table = self.parse_table(Delete)?; + query.conditions = self.parse_where()?; + Ok(query) } + + fn tokens(&mut self) -> &mut VecDeque { + &mut self.tokens + } + fn validate_keywords(&self) -> Result<(), InvalidSQL> { validate_keywords(ALLOWED_KEYWORDS, &self.tokens, Delete) } diff --git a/src/query/builder/expression.rs b/src/query/builder/expression.rs index c310a40..78bcd57 100644 --- a/src/query/builder/expression.rs +++ b/src/query/builder/expression.rs @@ -39,25 +39,18 @@ pub enum ExpressionNode { } impl ExpressionBuilder { - pub fn new() -> Self { - Self + pub fn parse_expressions(tokens: &mut VecDeque) -> Result { + ExpressionBuilder::parse_or(tokens) } - pub fn parse_expressions( - &self, - tokens: &mut VecDeque, - ) -> Result { - self.parse_or(tokens) - } - - fn parse_or(&self, tokens: &mut VecDeque) -> Result { - let mut left = self.parse_and(tokens)?; + fn parse_or(tokens: &mut VecDeque) -> Result { + let mut left = ExpressionBuilder::parse_and(tokens)?; while let Some(t) = tokens.front() { if t.kind != Keyword || t.value != "OR" { break; } tokens.pop_front(); - let right = self.parse_and(tokens)?; + let right = ExpressionBuilder::parse_and(tokens)?; left = ExpressionNode::Statement { operator: ExpressionOperator::Or, left: Box::new(left), @@ -67,14 +60,14 @@ impl ExpressionBuilder { Ok(left) } - fn parse_and(&self, tokens: &mut VecDeque) -> Result { - let mut left = self.parse_not(tokens)?; + fn parse_and(tokens: &mut VecDeque) -> Result { + let mut left = ExpressionBuilder::parse_not(tokens)?; while let Some(t) = tokens.front() { if t.kind != Keyword || t.value != "AND" { break; } tokens.pop_front(); - let right = self.parse_not(tokens)?; + let right = ExpressionBuilder::parse_not(tokens)?; left = ExpressionNode::Statement { operator: ExpressionOperator::And, left: Box::new(left), @@ -84,11 +77,11 @@ impl ExpressionBuilder { Ok(left) } - fn parse_not(&self, tokens: &mut VecDeque) -> Result { + fn parse_not(tokens: &mut VecDeque) -> Result { if let Some(t) = tokens.front() { if t.kind == Keyword && t.value == "NOT" { tokens.pop_front(); - let node = self.parse_comparisons(tokens)?; + let node = ExpressionBuilder::parse_comparisons(tokens)?; return Ok(ExpressionNode::Statement { operator: ExpressionOperator::Not, left: Box::new(node), @@ -96,17 +89,31 @@ impl ExpressionBuilder { }); } } - self.parse_comparisons(tokens) + ExpressionBuilder::parse_comparisons(tokens) + } + + fn parse_comparisons(tokens: &mut VecDeque) -> Result { + let left = ExpressionBuilder::parse_leaf(tokens)?; + let operator = ExpressionBuilder::parse_simple_operator(tokens); + if operator.is_err() { + return Ok(left); + } + let right = ExpressionBuilder::parse_leaf(tokens)?; + Ok(ExpressionNode::Statement { + operator: operator?, + left: Box::new(left), + right: Box::new(right), + }) } - fn parse_leaf(&self, tokens: &mut VecDeque) -> Result { + fn parse_leaf(tokens: &mut VecDeque) -> Result { let t = tokens .front() .ok_or_else(|| Syntax("reached end of query while parsing comparisons.".to_string()))?; match t.kind { TokenKind::ParenthesisOpen => { tokens.pop_front(); - let expression = self.parse_expressions(tokens)?; + let expression = ExpressionBuilder::parse_expressions(tokens)?; tokens .front() .filter(|t| t.kind == ParenthesisClose) @@ -129,7 +136,6 @@ impl ExpressionBuilder { } fn parse_simple_operator( - &self, tokens: &mut VecDeque, ) -> Result { let t = tokens @@ -147,21 +153,4 @@ impl ExpressionBuilder { tokens.pop_front(); Ok(op) } - - fn parse_comparisons( - &self, - tokens: &mut VecDeque, - ) -> Result { - let left = self.parse_leaf(tokens)?; - let operator = self.parse_simple_operator(tokens); - if operator.is_err() { - return Ok(left); - } - let right = self.parse_leaf(tokens)?; - Ok(ExpressionNode::Statement { - operator: operator?, - left: Box::new(left), - right: Box::new(right), - }) - } } diff --git a/src/query/builder/insert.rs b/src/query/builder/insert.rs index 8996c1e..7a0cbe4 100644 --- a/src/query/builder/insert.rs +++ b/src/query/builder/insert.rs @@ -1,7 +1,9 @@ +use crate::errored; use crate::query::builder::{validate_keywords, Builder}; use crate::query::errors::InvalidSQL; +use crate::query::errors::InvalidSQL::Syntax; use crate::query::Operation::Insert; -use crate::query::{Query, Token}; +use crate::query::{Query, Token, TokenKind}; use std::collections::VecDeque; const ALLOWED_KEYWORDS: &[&str] = &["VALUES"]; @@ -14,16 +16,51 @@ impl InsertBuilder { pub fn new(tokens: VecDeque) -> Self { Self { tokens } } + + fn parse_insert_values(&mut self) -> Result, InvalidSQL> { + self.expect_keyword("VALUES")?; + let mut closed_values = false; + let mut values = vec![]; + while let Some(t) = self.tokens.front() { + if closed_values { + errored!(Syntax, "invalid tokens after insert value: {:?}", t) + } + match t.kind { + TokenKind::String | TokenKind::Number => { + if let Some(token) = self.tokens.pop_front() { + values.push(token); + } + } + TokenKind::ParenthesisOpen => { + self.tokens.pop_front(); + } + TokenKind::ParenthesisClose => { + self.tokens.pop_front(); + closed_values = true; + } + _ => {} + } + } + Ok(values) + } } impl Builder for InsertBuilder { fn build(&mut self) -> Result { let mut query = Query::default(); - query.operation = Insert; self.validate_keywords()?; + query.operation = Insert; + query.table = self.parse_table(Insert)?; + query.columns = self.parse_columns()?; + query.inserts = self.parse_insert_values()?; Ok(query) } + + fn tokens(&mut self) -> &mut VecDeque { + &mut self.tokens + } + fn validate_keywords(&self) -> Result<(), InvalidSQL> { validate_keywords(ALLOWED_KEYWORDS, &self.tokens, Insert) } diff --git a/src/query/builder/mod.rs b/src/query/builder/mod.rs index 73aa3c6..aa0de20 100644 --- a/src/query/builder/mod.rs +++ b/src/query/builder/mod.rs @@ -6,18 +6,102 @@ mod update; use crate::errored; use crate::query::builder::delete::DeleteBuilder; +use crate::query::builder::expression::{ExpressionBuilder, ExpressionNode}; use crate::query::builder::insert::InsertBuilder; use crate::query::builder::select::SelectBuilder; use crate::query::builder::update::UpdateBuilder; use crate::query::errors::InvalidSQL; use crate::query::errors::InvalidSQL::Syntax; use crate::query::Operation::{Delete, Insert, Select, Unknown, Update}; -use crate::query::TokenKind::Keyword; +use crate::query::TokenKind::{Identifier, Keyword, Operator, ParenthesisClose, ParenthesisOpen}; use crate::query::{Operation, Query, Token}; use std::collections::VecDeque; pub trait Builder { fn build(&mut self) -> Result; + fn tokens(&mut self) -> &mut VecDeque; + + fn parse_table(&mut self, operation: Operation) -> Result { + if let Some(t) = self.tokens().front() { + match operation { + Select | Delete => { + if t.kind != Keyword || t.value != "FROM" { + errored!(Syntax, "missing FROM clause, got: {}", t.value) + } + self.tokens().pop_front(); + } + Update | Insert => { + if t.kind != Identifier { + errored!(Syntax, "expected table name, got: {:?}", t) + } + } + _ => { + errored!(Syntax, "unexpected query operation, got: {:?}", t) + } + } + } + match self.tokens().pop_front() { + None => errored!(Syntax, "could not find table identifier."), + Some(t) => { + if t.kind != Identifier { + unexpected_token_in_stage("TABLE".to_string(), &t)? + } + Ok(t.value) + } + } + } + + fn parse_columns(&mut self) -> Result, InvalidSQL> { + let mut fields: Vec = vec![]; + while let Some(t) = self.tokens().front() { + match t.kind { + Identifier => { + if let Some(op) = self.tokens().pop_front() { + fields.push(op); + } + } + Operator if t.value == "*" => { + if let Some(op) = self.tokens().pop_front() { + fields.push(op); + break; + } + } + Keyword if t.value == "FROM" || t.value == "VALUES" => { + break; + } + ParenthesisOpen => { + self.tokens().pop_front(); + } + ParenthesisClose => { + self.tokens().pop_front(); + break; + } + _ => unexpected_token_in_stage("COLUMN".to_string(), t)?, + } + } + Ok(fields) + } + + fn parse_where(&mut self) -> Result { + self.expect_keyword("WHERE")?; + ExpressionBuilder::parse_expressions(self.tokens()) + } + + fn expect_keyword(&mut self, keyword: &str) -> Result<(), InvalidSQL> { + if let Some(t) = self.tokens().front() { + if t.kind != Keyword || t.value != keyword.to_uppercase() { + errored!( + Syntax, + "missing {} clause, got: {}", + keyword.to_uppercase(), + t.value + ) + } + self.tokens().pop_front(); + } + Ok(()) + } + fn validate_keywords(&self) -> Result<(), InvalidSQL>; } diff --git a/src/query/builder/select.rs b/src/query/builder/select.rs index 267223f..2c449c9 100644 --- a/src/query/builder/select.rs +++ b/src/query/builder/select.rs @@ -1,8 +1,5 @@ -use crate::errored; -use crate::query::builder::expression::{ExpressionBuilder, ExpressionNode}; use crate::query::builder::{unexpected_token_in_stage, validate_keywords, Builder}; use crate::query::errors::InvalidSQL; -use crate::query::errors::InvalidSQL::Syntax; use crate::query::Operation::Select; use crate::query::OrderKind::{Asc, Desc}; use crate::query::TokenKind::{Identifier, Keyword, Operator}; @@ -15,68 +12,16 @@ const ALLOWED_KEYWORDS: &[&str] = &[ pub struct SelectBuilder { tokens: VecDeque, - where_builder: ExpressionBuilder, } impl SelectBuilder { pub fn new(tokens: VecDeque) -> Self { - Self { - tokens, - where_builder: ExpressionBuilder::new(), - } - } - - fn parse_table(&mut self) -> Result { - if let Some(t) = self.tokens.front() { - if t.kind != Keyword || t.value != "FROM" { - errored!(Syntax, "missing FROM clause, got: {}", t.value) - } - self.tokens.pop_front(); - } - match self.tokens.pop_front() { - None => errored!(Syntax, "could not find table identifier."), - Some(t) => { - if t.kind != Identifier { - unexpected_token_in_stage("TABLE".to_string(), &t)? - } - Ok(t.value) - } - } - } - - fn parse_fields(&mut self) -> Result, InvalidSQL> { - let mut fields: Vec = vec![]; - while let Some(t) = self.tokens.front() { - match t.kind { - Identifier => { - if let Some(op) = self.tokens.pop_front() { - fields.push(op); - } - } - Operator if t.value == "*" => { - if let Some(op) = self.tokens.pop_front() { - fields.push(op); - break; - } - } - Keyword if t.value == "FROM" => { - break; - } - _ => unexpected_token_in_stage("SELECT".to_string(), t)?, - } - } - Ok(fields) + Self { tokens } } fn parse_ordering(&mut self) -> Result, InvalidSQL> { let mut ordering = vec![]; - if self - .tokens - .front() - .map_or(false, |t| t.kind == Keyword && t.value == "ORDER BY") - { - self.tokens.pop_front(); - } else { + if self.expect_keyword("ORDER BY").is_err() { return Ok(ordering); } while let Some(t) = self.tokens.pop_front() { @@ -98,16 +43,6 @@ impl SelectBuilder { } Ok(ordering) } - - fn parse_conditions(&mut self) -> Result { - if let Some(t) = self.tokens.front() { - if t.kind != Keyword || t.value != "WHERE" { - errored!(Syntax, "missing WHERE clause, got: {}", t.value) - } - self.tokens.pop_front(); - } - self.where_builder.parse_expressions(&mut self.tokens) - } } impl Builder for SelectBuilder { @@ -116,14 +51,18 @@ impl Builder for SelectBuilder { self.validate_keywords()?; query.operation = Select; - query.fields = self.parse_fields()?; - query.table = self.parse_table()?; - query.expressions = self.parse_conditions()?; + query.columns = self.parse_columns()?; + query.table = self.parse_table(Select)?; + query.conditions = self.parse_where()?; query.ordering = self.parse_ordering()?; Ok(query) } + fn tokens(&mut self) -> &mut VecDeque { + &mut self.tokens + } + fn validate_keywords(&self) -> Result<(), InvalidSQL> { validate_keywords(ALLOWED_KEYWORDS, &self.tokens, Select) } diff --git a/src/query/builder/update.rs b/src/query/builder/update.rs index 0473b9f..d7081f7 100644 --- a/src/query/builder/update.rs +++ b/src/query/builder/update.rs @@ -1,6 +1,10 @@ +use crate::errored; +use crate::query::builder::expression::{ExpressionBuilder, ExpressionNode}; use crate::query::builder::{validate_keywords, Builder}; use crate::query::errors::InvalidSQL; +use crate::query::errors::InvalidSQL::Syntax; use crate::query::Operation::Update; +use crate::query::TokenKind::Keyword; use crate::query::{Query, Token}; use std::collections::VecDeque; @@ -14,17 +18,44 @@ impl UpdateBuilder { pub fn new(tokens: VecDeque) -> Self { Self { tokens } } + + fn parse_updates(&mut self) -> Result, InvalidSQL> { + self.expect_keyword("SET")?; + let mut updates = vec![]; + while let Some(t) = self.tokens.front() { + if t.kind != Keyword && t.value != "WHERE" { + let update = ExpressionBuilder::parse_expressions(&mut self.tokens)?; + match update { + ExpressionNode::Statement { .. } => updates.push(update), + _ => errored!( + Syntax, + "failed to parse update statement, got: {:?}", + update + ), + } + } else { + break; + } + } + Ok(updates) + } } impl Builder for UpdateBuilder { fn build(&mut self) -> Result { let mut query = Query::default(); - query.operation = Update; self.validate_keywords()?; - + query.operation = Update; + query.table = self.parse_table(Update)?; + query.updates = self.parse_updates()?; + query.conditions = self.parse_where()?; Ok(query) } + fn tokens(&mut self) -> &mut VecDeque { + &mut self.tokens + } + fn validate_keywords(&self) -> Result<(), InvalidSQL> { validate_keywords(ALLOWED_KEYWORDS, &self.tokens, Update) } diff --git a/src/query/mod.rs b/src/query/mod.rs index 6217b88..2032de3 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -12,8 +12,10 @@ use std::fmt::{Debug, Display, Formatter}; pub struct Query { pub operation: Operation, pub table: String, - fields: Vec, - expressions: ExpressionNode, + columns: Vec, + inserts: Vec, + updates: Vec, + conditions: ExpressionNode, ordering: Vec, } @@ -78,8 +80,10 @@ impl Query { Self { operation: Operation::Unknown, table: "".to_string(), - fields: vec![], - expressions: ExpressionNode::default(), + columns: vec![], + inserts: vec![], + updates: vec![], + conditions: ExpressionNode::default(), ordering: vec![], } } @@ -87,11 +91,14 @@ impl Query { impl Display for Query { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let fields: Vec<&str> = self.columns.iter().map(|f| f.value.as_str()).collect(); + let inserts: Vec<&str> = self.inserts.iter().map(|f| f.value.as_str()).collect(); writeln!(f, "Query Kind: [{:?}]", self.operation)?; writeln!(f, "Table: {:?}", self.table)?; - let fields: Vec<&str> = self.fields.iter().map(|f| f.value.as_str()).collect(); - writeln!(f, "Fields: {:?}", fields)?; - writeln!(f, "Expressions: {:?}", self.expressions)?; + writeln!(f, "Columns: {:?}", fields)?; + writeln!(f, "Inserts: {:?}", inserts)?; + writeln!(f, "Updates: {:?}", self.updates)?; + writeln!(f, "Conditions: {:?}", self.conditions)?; writeln!(f, "Ordering: {:?}", self.ordering) } } From 4a591487f8a7e2e6a5c02207a8a18d3e007e5ca6 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Fri, 6 Sep 2024 21:39:58 -0300 Subject: [PATCH 037/103] slight refactoring --- src/query/builder/delete.rs | 4 +--- src/query/builder/insert.rs | 14 +++++++----- src/query/builder/mod.rs | 45 +++++++++++++++++++------------------ src/query/builder/select.rs | 13 +++++------ src/query/builder/update.rs | 2 +- src/query/mod.rs | 13 ++++++++++- src/query/tokenizer.rs | 2 +- 7 files changed, 51 insertions(+), 42 deletions(-) diff --git a/src/query/builder/delete.rs b/src/query/builder/delete.rs index c209574..7ae4dd4 100644 --- a/src/query/builder/delete.rs +++ b/src/query/builder/delete.rs @@ -1,4 +1,4 @@ -use crate::query::builder::{unexpected_token_in_stage, validate_keywords, Builder}; +use crate::query::builder::{validate_keywords, Builder}; use crate::query::errors::InvalidSQL; use crate::query::Operation::Delete; use crate::query::{Query, Token}; @@ -20,11 +20,9 @@ impl Builder for DeleteBuilder { fn build(&mut self) -> Result { let mut query = Query::default(); self.validate_keywords()?; - query.operation = Delete; query.table = self.parse_table(Delete)?; query.conditions = self.parse_where()?; - Ok(query) } diff --git a/src/query/builder/insert.rs b/src/query/builder/insert.rs index 7a0cbe4..9ab6abc 100644 --- a/src/query/builder/insert.rs +++ b/src/query/builder/insert.rs @@ -1,8 +1,9 @@ use crate::errored; -use crate::query::builder::{validate_keywords, Builder}; +use crate::query::builder::{unexpected_token_in_stage, validate_keywords, Builder}; use crate::query::errors::InvalidSQL; use crate::query::errors::InvalidSQL::Syntax; use crate::query::Operation::Insert; +use crate::query::TokenKind::{Keyword, ParenthesisClose, ParenthesisOpen}; use crate::query::{Query, Token, TokenKind}; use std::collections::VecDeque; @@ -18,7 +19,8 @@ impl InsertBuilder { } fn parse_insert_values(&mut self) -> Result, InvalidSQL> { - self.expect_keyword("VALUES")?; + self.pop_expecting("VALUES", Keyword)?; + self.peek_expecting("(", ParenthesisOpen)?; let mut closed_values = false; let mut values = vec![]; while let Some(t) = self.tokens.front() { @@ -31,14 +33,14 @@ impl InsertBuilder { values.push(token); } } - TokenKind::ParenthesisOpen => { + ParenthesisOpen => { self.tokens.pop_front(); } - TokenKind::ParenthesisClose => { + ParenthesisClose => { self.tokens.pop_front(); closed_values = true; } - _ => {} + _ => unexpected_token_in_stage("VALUES", t)?, } } Ok(values) @@ -51,9 +53,9 @@ impl Builder for InsertBuilder { self.validate_keywords()?; query.operation = Insert; query.table = self.parse_table(Insert)?; + self.peek_expecting("(", ParenthesisOpen)?; query.columns = self.parse_columns()?; query.inserts = self.parse_insert_values()?; - Ok(query) } diff --git a/src/query/builder/mod.rs b/src/query/builder/mod.rs index aa0de20..ac131c7 100644 --- a/src/query/builder/mod.rs +++ b/src/query/builder/mod.rs @@ -14,7 +14,7 @@ use crate::query::errors::InvalidSQL; use crate::query::errors::InvalidSQL::Syntax; use crate::query::Operation::{Delete, Insert, Select, Unknown, Update}; use crate::query::TokenKind::{Identifier, Keyword, Operator, ParenthesisClose, ParenthesisOpen}; -use crate::query::{Operation, Query, Token}; +use crate::query::{Operation, Query, Token, TokenKind}; use std::collections::VecDeque; pub trait Builder { @@ -30,11 +30,7 @@ pub trait Builder { } self.tokens().pop_front(); } - Update | Insert => { - if t.kind != Identifier { - errored!(Syntax, "expected table name, got: {:?}", t) - } - } + Update | Insert => {} _ => { errored!(Syntax, "unexpected query operation, got: {:?}", t) } @@ -44,7 +40,7 @@ pub trait Builder { None => errored!(Syntax, "could not find table identifier."), Some(t) => { if t.kind != Identifier { - unexpected_token_in_stage("TABLE".to_string(), &t)? + unexpected_token_in_stage("TABLE", &t)? } Ok(t.value) } @@ -76,28 +72,34 @@ pub trait Builder { self.tokens().pop_front(); break; } - _ => unexpected_token_in_stage("COLUMN".to_string(), t)?, + _ => unexpected_token_in_stage("COLUMN", t)?, } } Ok(fields) } fn parse_where(&mut self) -> Result { - self.expect_keyword("WHERE")?; + self.pop_expecting("WHERE", Keyword)?; ExpressionBuilder::parse_expressions(self.tokens()) } - fn expect_keyword(&mut self, keyword: &str) -> Result<(), InvalidSQL> { + fn pop_expecting(&mut self, value: &str, kind: TokenKind) -> Result<(), InvalidSQL> { + self.peek_expecting(value, kind)?; + self.tokens().pop_front(); + Ok(()) + } + + fn peek_expecting(&mut self, value: &str, kind: TokenKind) -> Result<(), InvalidSQL> { + let expected = Token { + value: value.to_string(), + kind, + }; if let Some(t) = self.tokens().front() { - if t.kind != Keyword || t.value != keyword.to_uppercase() { - errored!( - Syntax, - "missing {} clause, got: {}", - keyword.to_uppercase(), - t.value - ) + if t.kind != expected.kind || t.value != expected.value.to_uppercase() { + errored!(Syntax, "expected {:?} token, got: {:?}", expected, t) } - self.tokens().pop_front(); + } else { + errored!(Syntax, "got None when expecting: {:?}", expected) } Ok(()) } @@ -151,12 +153,11 @@ fn validate_keywords( Ok(()) } -fn unexpected_token_in_stage(stage: String, token: &Token) -> Result<(), InvalidSQL> { +pub fn unexpected_token_in_stage(stage: &str, token: &Token) -> Result<(), InvalidSQL> { errored!( Syntax, - "unexpected token while parsing {} fields: {} of kind {:?}", + "unexpected token while parsing {} fields: [{:?}]", stage, - token.value, - token.kind + token ) } diff --git a/src/query/builder/select.rs b/src/query/builder/select.rs index 2c449c9..ec5f5c8 100644 --- a/src/query/builder/select.rs +++ b/src/query/builder/select.rs @@ -2,7 +2,7 @@ use crate::query::builder::{unexpected_token_in_stage, validate_keywords, Builde use crate::query::errors::InvalidSQL; use crate::query::Operation::Select; use crate::query::OrderKind::{Asc, Desc}; -use crate::query::TokenKind::{Identifier, Keyword, Operator}; +use crate::query::TokenKind::{Identifier, Keyword}; use crate::query::{Ordering, Query, Token}; use std::collections::VecDeque; @@ -21,12 +21,9 @@ impl SelectBuilder { fn parse_ordering(&mut self) -> Result, InvalidSQL> { let mut ordering = vec![]; - if self.expect_keyword("ORDER BY").is_err() { - return Ok(ordering); - } while let Some(t) = self.tokens.pop_front() { if t.kind != Identifier { - unexpected_token_in_stage("ORDER_BY".to_string(), &t)? + unexpected_token_in_stage("ORDER_BY", &t)? } let mut new_order = Ordering::default(); new_order.field = t; @@ -49,13 +46,13 @@ impl Builder for SelectBuilder { fn build(&mut self) -> Result { let mut query = Query::default(); self.validate_keywords()?; - query.operation = Select; query.columns = self.parse_columns()?; query.table = self.parse_table(Select)?; query.conditions = self.parse_where()?; - query.ordering = self.parse_ordering()?; - + if self.pop_expecting("ORDER BY", Keyword).is_ok() { + query.ordering = self.parse_ordering()?; + } Ok(query) } diff --git a/src/query/builder/update.rs b/src/query/builder/update.rs index d7081f7..02f13ed 100644 --- a/src/query/builder/update.rs +++ b/src/query/builder/update.rs @@ -20,7 +20,7 @@ impl UpdateBuilder { } fn parse_updates(&mut self) -> Result, InvalidSQL> { - self.expect_keyword("SET")?; + self.pop_expecting("SET", Keyword)?; let mut updates = vec![]; while let Some(t) = self.tokens.front() { if t.kind != Keyword && t.value != "WHERE" { diff --git a/src/query/mod.rs b/src/query/mod.rs index 2032de3..1e7e468 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -19,7 +19,6 @@ pub struct Query { ordering: Vec, } -#[derive(Debug)] pub struct Token { pub value: String, pub kind: TokenKind, @@ -115,6 +114,18 @@ impl Debug for Ordering { } } +impl Display for Token { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "[{}:{:?}]", self.value, self.kind) + } +} + +impl Debug for Token { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self) + } +} + impl Debug for ExpressionNode { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { diff --git a/src/query/tokenizer.rs b/src/query/tokenizer.rs index 08b2009..c4a4878 100644 --- a/src/query/tokenizer.rs +++ b/src/query/tokenizer.rs @@ -31,7 +31,7 @@ const RESERVED_KEYWORDS: &[&str] = &[ pub struct Tokenizer { i: usize, state: TokenizerState, - parenthesis_count: u8, + parenthesis_count: i8, } enum TokenizerState { From c0865d22c541ff99ec34f83c8034be47d23e5e13 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Fri, 6 Sep 2024 21:42:30 -0300 Subject: [PATCH 038/103] slight refactoring --- src/query/builder/insert.rs | 2 +- src/query/builder/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/query/builder/insert.rs b/src/query/builder/insert.rs index 9ab6abc..e9fd046 100644 --- a/src/query/builder/insert.rs +++ b/src/query/builder/insert.rs @@ -25,7 +25,7 @@ impl InsertBuilder { let mut values = vec![]; while let Some(t) = self.tokens.front() { if closed_values { - errored!(Syntax, "invalid tokens after insert value: {:?}", t) + unexpected_token_in_stage("AFTER VALUES", t)?; } match t.kind { TokenKind::String | TokenKind::Number => { diff --git a/src/query/builder/mod.rs b/src/query/builder/mod.rs index ac131c7..085697c 100644 --- a/src/query/builder/mod.rs +++ b/src/query/builder/mod.rs @@ -156,7 +156,7 @@ fn validate_keywords( pub fn unexpected_token_in_stage(stage: &str, token: &Token) -> Result<(), InvalidSQL> { errored!( Syntax, - "unexpected token while parsing {} fields: [{:?}]", + "unexpected token while parsing {} fields: {:?}", stage, token ) From 838bf6970a882f8ae237a5b9ce893401e04c1803 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Fri, 6 Sep 2024 21:54:50 -0300 Subject: [PATCH 039/103] fixed bug in select --- src/query/builder/mod.rs | 5 ++--- src/query/builder/select.rs | 12 ++++++++++-- src/query/mod.rs | 13 +------------ 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/query/builder/mod.rs b/src/query/builder/mod.rs index 085697c..c30d8ce 100644 --- a/src/query/builder/mod.rs +++ b/src/query/builder/mod.rs @@ -83,10 +83,9 @@ pub trait Builder { ExpressionBuilder::parse_expressions(self.tokens()) } - fn pop_expecting(&mut self, value: &str, kind: TokenKind) -> Result<(), InvalidSQL> { + fn pop_expecting(&mut self, value: &str, kind: TokenKind) -> Result, InvalidSQL> { self.peek_expecting(value, kind)?; - self.tokens().pop_front(); - Ok(()) + Ok(self.tokens().pop_front()) } fn peek_expecting(&mut self, value: &str, kind: TokenKind) -> Result<(), InvalidSQL> { diff --git a/src/query/builder/select.rs b/src/query/builder/select.rs index ec5f5c8..f19bc78 100644 --- a/src/query/builder/select.rs +++ b/src/query/builder/select.rs @@ -50,8 +50,16 @@ impl Builder for SelectBuilder { query.columns = self.parse_columns()?; query.table = self.parse_table(Select)?; query.conditions = self.parse_where()?; - if self.pop_expecting("ORDER BY", Keyword).is_ok() { - query.ordering = self.parse_ordering()?; + match self.peek_expecting("ORDER BY", Keyword) { + Ok(_) => { + self.tokens.pop_front(); + query.ordering = self.parse_ordering()?; + } + Err(_) => { + if let Some(t) = self.tokens.front() { + unexpected_token_in_stage("AFTER CONDITIONS", t)?; + } + } } Ok(query) } diff --git a/src/query/mod.rs b/src/query/mod.rs index 1e7e468..3eb7e0f 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -19,6 +19,7 @@ pub struct Query { ordering: Vec, } +#[derive(Debug, PartialEq)] pub struct Token { pub value: String, pub kind: TokenKind, @@ -114,18 +115,6 @@ impl Debug for Ordering { } } -impl Display for Token { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "[{}:{:?}]", self.value, self.kind) - } -} - -impl Debug for Token { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self) - } -} - impl Debug for ExpressionNode { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { From c82caea78ba1999cf84d3f21db5ff19ec73afb11 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sat, 7 Sep 2024 03:01:32 -0300 Subject: [PATCH 040/103] handle more edge cases for each query. --- src/query/builder/delete.rs | 8 +++++++- src/query/builder/insert.rs | 6 +----- src/query/builder/mod.rs | 7 +++++++ src/query/builder/select.rs | 10 ++++------ src/query/builder/update.rs | 9 +++++++-- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/query/builder/delete.rs b/src/query/builder/delete.rs index 7ae4dd4..5ecce6d 100644 --- a/src/query/builder/delete.rs +++ b/src/query/builder/delete.rs @@ -1,6 +1,7 @@ use crate::query::builder::{validate_keywords, Builder}; use crate::query::errors::InvalidSQL; use crate::query::Operation::Delete; +use crate::query::TokenKind::Keyword; use crate::query::{Query, Token}; use std::collections::VecDeque; @@ -22,7 +23,12 @@ impl Builder for DeleteBuilder { self.validate_keywords()?; query.operation = Delete; query.table = self.parse_table(Delete)?; - query.conditions = self.parse_where()?; + match self.peek_expecting("WHERE", Keyword) { + Ok(_) => { + query.conditions = self.parse_where()?; + } + Err(_) => self.expect_none()?, + } Ok(query) } diff --git a/src/query/builder/insert.rs b/src/query/builder/insert.rs index e9fd046..ed59a57 100644 --- a/src/query/builder/insert.rs +++ b/src/query/builder/insert.rs @@ -21,12 +21,8 @@ impl InsertBuilder { fn parse_insert_values(&mut self) -> Result, InvalidSQL> { self.pop_expecting("VALUES", Keyword)?; self.peek_expecting("(", ParenthesisOpen)?; - let mut closed_values = false; let mut values = vec![]; while let Some(t) = self.tokens.front() { - if closed_values { - unexpected_token_in_stage("AFTER VALUES", t)?; - } match t.kind { TokenKind::String | TokenKind::Number => { if let Some(token) = self.tokens.pop_front() { @@ -38,7 +34,6 @@ impl InsertBuilder { } ParenthesisClose => { self.tokens.pop_front(); - closed_values = true; } _ => unexpected_token_in_stage("VALUES", t)?, } @@ -56,6 +51,7 @@ impl Builder for InsertBuilder { self.peek_expecting("(", ParenthesisOpen)?; query.columns = self.parse_columns()?; query.inserts = self.parse_insert_values()?; + self.expect_none()?; Ok(query) } diff --git a/src/query/builder/mod.rs b/src/query/builder/mod.rs index c30d8ce..d2df8d9 100644 --- a/src/query/builder/mod.rs +++ b/src/query/builder/mod.rs @@ -83,6 +83,13 @@ pub trait Builder { ExpressionBuilder::parse_expressions(self.tokens()) } + fn expect_none(&mut self) -> Result<(), InvalidSQL> { + if let Some(t) = self.tokens().front() { + errored!(Syntax, "expected end of query but got: {:?}", t); + } + Ok(()) + } + fn pop_expecting(&mut self, value: &str, kind: TokenKind) -> Result, InvalidSQL> { self.peek_expecting(value, kind)?; Ok(self.tokens().pop_front()) diff --git a/src/query/builder/select.rs b/src/query/builder/select.rs index f19bc78..c93bad6 100644 --- a/src/query/builder/select.rs +++ b/src/query/builder/select.rs @@ -49,17 +49,15 @@ impl Builder for SelectBuilder { query.operation = Select; query.columns = self.parse_columns()?; query.table = self.parse_table(Select)?; - query.conditions = self.parse_where()?; + if self.peek_expecting("WHERE", Keyword).is_ok() { + query.conditions = self.parse_where()?; + } match self.peek_expecting("ORDER BY", Keyword) { Ok(_) => { self.tokens.pop_front(); query.ordering = self.parse_ordering()?; } - Err(_) => { - if let Some(t) = self.tokens.front() { - unexpected_token_in_stage("AFTER CONDITIONS", t)?; - } - } + Err(_) => self.expect_none()?, } Ok(query) } diff --git a/src/query/builder/update.rs b/src/query/builder/update.rs index 02f13ed..b75fa32 100644 --- a/src/query/builder/update.rs +++ b/src/query/builder/update.rs @@ -1,6 +1,6 @@ use crate::errored; use crate::query::builder::expression::{ExpressionBuilder, ExpressionNode}; -use crate::query::builder::{validate_keywords, Builder}; +use crate::query::builder::{unexpected_token_in_stage, validate_keywords, Builder}; use crate::query::errors::InvalidSQL; use crate::query::errors::InvalidSQL::Syntax; use crate::query::Operation::Update; @@ -48,7 +48,12 @@ impl Builder for UpdateBuilder { query.operation = Update; query.table = self.parse_table(Update)?; query.updates = self.parse_updates()?; - query.conditions = self.parse_where()?; + match self.peek_expecting("WHERE", Keyword) { + Ok(_) => { + query.conditions = self.parse_where()?; + } + Err(_) => self.expect_none()?, + } Ok(query) } From db98cb0f2e699b311e98fb2fc2050196b800f21b Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sat, 7 Sep 2024 03:34:17 -0300 Subject: [PATCH 041/103] slight refactor to parse_leaf --- src/query/builder/expression.rs | 49 +++++++++++++++------------------ 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/src/query/builder/expression.rs b/src/query/builder/expression.rs index 78bcd57..f00f49e 100644 --- a/src/query/builder/expression.rs +++ b/src/query/builder/expression.rs @@ -1,11 +1,11 @@ use crate::errored; -use crate::query::builder::expression::ExpressionNode::Leaf; +use crate::query::builder::expression::ExpressionNode::{Empty, Leaf}; use crate::query::builder::expression::ExpressionOperator::{ Equals, GreaterOrEqual, GreaterThan, LessOrEqual, LessThan, NotEquals, }; use crate::query::errors::InvalidSQL; use crate::query::errors::InvalidSQL::Syntax; -use crate::query::TokenKind::{Keyword, ParenthesisClose}; +use crate::query::TokenKind::Keyword; use crate::query::{Token, TokenKind}; use std::collections::VecDeque; @@ -26,7 +26,7 @@ pub enum ExpressionOperator { Not, } -#[derive(Default)] +#[derive(Default, PartialEq)] pub enum ExpressionNode { #[default] Empty, @@ -107,32 +107,27 @@ impl ExpressionBuilder { } fn parse_leaf(tokens: &mut VecDeque) -> Result { - let t = tokens - .front() - .ok_or_else(|| Syntax("reached end of query while parsing comparisons.".to_string()))?; - match t.kind { - TokenKind::ParenthesisOpen => { - tokens.pop_front(); - let expression = ExpressionBuilder::parse_expressions(tokens)?; - tokens - .front() - .filter(|t| t.kind == ParenthesisClose) - .ok_or_else(|| { - Syntax("unclosed parenthesis while evaluating WHERE.".to_string()) - })?; - tokens.pop_front(); - Ok(expression) + let mut leaf = Empty; + while let Some(t) = tokens.front() { + match t.kind { + TokenKind::Identifier | TokenKind::Number | TokenKind::String => { + if let Some(t) = tokens.pop_front() { + leaf = Leaf(t); + break; + } + } + TokenKind::ParenthesisOpen => { + tokens.pop_front(); + leaf = ExpressionBuilder::parse_expressions(tokens)?; + } + TokenKind::ParenthesisClose if leaf != Empty => { + tokens.pop_front(); + break; + } + _ => errored!(Syntax, "unexpected token when parsing leaf: {:?}", t), } - TokenKind::Identifier | TokenKind::Number | TokenKind::String => tokens - .pop_front() - .map(Leaf) - .ok_or_else(|| Syntax("failed to parse leaf node".to_string())), - _ => errored!( - Syntax, - "unrecognized token while parsing comparison: {:?}.", - t - ), } + Ok(leaf) } fn parse_simple_operator( From 2137d352cd02152d1d80134ce6c286dea597d166 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sat, 7 Sep 2024 18:44:23 -0300 Subject: [PATCH 042/103] more refactors --- src/main.rs | 21 +++++----------- src/query/builder/mod.rs | 36 ++++++++++------------------ src/query/executor/mod.rs | 11 +++++++++ src/query/mod.rs | 4 ++-- src/{ => utils}/errors.rs | 0 src/{files/mod.rs => utils/files.rs} | 8 +++---- src/utils/mod.rs | 2 ++ 7 files changed, 36 insertions(+), 46 deletions(-) rename src/{ => utils}/errors.rs (100%) rename src/{files/mod.rs => utils/files.rs} (77%) create mode 100644 src/utils/mod.rs diff --git a/src/main.rs b/src/main.rs index 5e6e3f1..dd20ea4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,12 @@ +use crate::query::executor::Executor; use crate::query::tokenizer::Tokenizer; use crate::query::Query; -use files::validate_path; use query::validate_query_string; use std::env; use std::error::Error; -mod errors; -mod files; mod query; +mod utils; fn main() -> Result<(), Box> { let args = env::args().collect(); @@ -17,7 +16,7 @@ fn main() -> Result<(), Box> { Ok(()) } -fn run(args: Vec) -> Result<(), Box> { +fn run(args: Vec) -> Result, Box> { if args.len() != 3 { println!("invalid usage of rustic-sql"); return Err("usage: cargo run -- ".into()); @@ -25,19 +24,11 @@ fn run(args: Vec) -> Result<(), Box> { let path: &String = &args[1]; let query: &String = &args[2]; - - validate_path(path)?; validate_query_string(query)?; - let mut tokenizer = Tokenizer::new(); - - let tokens = tokenizer.tokenize(query)?; - for t in &tokens { - println!("{:?}", &t) - } - + let tokens = Tokenizer::new().tokenize(query)?; let query = Query::from(tokens)?; println!("\n{:?}", &query); - - Ok(()) + let result = Executor::run(path, query)?; + Ok(result) } diff --git a/src/query/builder/mod.rs b/src/query/builder/mod.rs index d2df8d9..92e30c0 100644 --- a/src/query/builder/mod.rs +++ b/src/query/builder/mod.rs @@ -25,26 +25,20 @@ pub trait Builder { if let Some(t) = self.tokens().front() { match operation { Select | Delete => { - if t.kind != Keyword || t.value != "FROM" { - errored!(Syntax, "missing FROM clause, got: {}", t.value) - } + self.peek_expecting("FROM", Keyword)?; self.tokens().pop_front(); } - Update | Insert => {} - _ => { - errored!(Syntax, "unexpected query operation, got: {:?}", t) - } + _ => {} } } - match self.tokens().pop_front() { - None => errored!(Syntax, "could not find table identifier."), - Some(t) => { - if t.kind != Identifier { - unexpected_token_in_stage("TABLE", &t)? - } - Ok(t.value) - } + let t = self + .tokens() + .pop_front() + .ok_or_else(|| Syntax("could not find table identifier.".to_string()))?; + if t.kind != Identifier { + unexpected_token_in_stage("TABLE", &t)?; } + Ok(t.value) } fn parse_columns(&mut self) -> Result, InvalidSQL> { @@ -56,21 +50,15 @@ pub trait Builder { fields.push(op); } } - Operator if t.value == "*" => { - if let Some(op) = self.tokens().pop_front() { - fields.push(op); - break; - } - } Keyword if t.value == "FROM" || t.value == "VALUES" => { break; } - ParenthesisOpen => { + ParenthesisClose | Operator if t.value == "*" => { self.tokens().pop_front(); + break; } - ParenthesisClose => { + ParenthesisOpen => { self.tokens().pop_front(); - break; } _ => unexpected_token_in_stage("COLUMN", t)?, } diff --git a/src/query/executor/mod.rs b/src/query/executor/mod.rs index 24b704e..e2c8f17 100644 --- a/src/query/executor/mod.rs +++ b/src/query/executor/mod.rs @@ -1,4 +1,15 @@ +use crate::query::errors::InvalidSQL; +use crate::query::Query; + mod delete; mod insert; mod select; mod update; + +pub struct Executor; + +impl Executor { + pub fn run(path: &str, query: Query) -> Result, InvalidSQL> { + Ok(vec![]) + } +} diff --git a/src/query/mod.rs b/src/query/mod.rs index 3eb7e0f..2373781 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -1,12 +1,12 @@ pub mod builder; mod errors; -mod executor; +pub mod executor; pub mod tokenizer; -use crate::errors::Errored; use crate::query::builder::expression::ExpressionNode; use crate::query::OrderKind::Asc; use crate::query::TokenKind::Unknown; +use crate::utils::errors::Errored; use std::fmt::{Debug, Display, Formatter}; pub struct Query { diff --git a/src/errors.rs b/src/utils/errors.rs similarity index 100% rename from src/errors.rs rename to src/utils/errors.rs diff --git a/src/files/mod.rs b/src/utils/files.rs similarity index 77% rename from src/files/mod.rs rename to src/utils/files.rs index 166ec36..e3abb93 100644 --- a/src/files/mod.rs +++ b/src/utils/files.rs @@ -1,10 +1,9 @@ use crate::errored; -use crate::errors::Errored; +use crate::utils::errors::Errored; use std::path::Path; -pub fn validate_path(dir: &str) -> Result<(), Errored> { +pub fn validate_path(dir: &str) -> Result<&Path, Errored> { let path = Path::new(dir); - if !path.exists() { errored!(Errored, "path '{dir}' does not exist"); } else if !path.is_dir() { @@ -12,6 +11,5 @@ pub fn validate_path(dir: &str) -> Result<(), Errored> { } else if path.read_dir()?.next().is_none() { errored!(Errored, "path '{dir}' is an empty directory"); } - - Ok(()) + Ok(path) } diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..ca605cf --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod errors; +mod files; From a97842d1f71bbe3b99ccdb2dafc383ccf2fd1e3f Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sat, 7 Sep 2024 21:21:55 -0300 Subject: [PATCH 043/103] module refactor to match tp requirements --- src/main.rs | 4 +- src/query/builder/delete.rs | 7 +- src/query/builder/expression.rs | 42 ++--------- src/query/builder/insert.rs | 9 +-- src/query/builder/mod.rs | 41 ++++------ src/query/builder/select.rs | 10 ++- src/query/builder/update.rs | 12 +-- src/query/executor/delete.rs | 7 ++ src/query/executor/insert.rs | 7 ++ src/query/executor/mod.rs | 11 ++- src/query/executor/select.rs | 7 ++ src/query/executor/update.rs | 7 ++ src/query/mod.rs | 130 +------------------------------- src/query/structs/expression.rs | 45 +++++++++++ src/query/structs/mod.rs | 5 ++ src/query/structs/operation.rs | 8 ++ src/query/structs/ordering.rs | 29 +++++++ src/query/structs/query.rs | 71 +++++++++++++++++ src/query/structs/token.rs | 28 +++++++ src/query/tokenizer.rs | 6 +- 20 files changed, 274 insertions(+), 212 deletions(-) create mode 100644 src/query/structs/expression.rs create mode 100644 src/query/structs/mod.rs create mode 100644 src/query/structs/operation.rs create mode 100644 src/query/structs/ordering.rs create mode 100644 src/query/structs/query.rs create mode 100644 src/query/structs/token.rs diff --git a/src/main.rs b/src/main.rs index dd20ea4..f3efd9c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use crate::query::executor::Executor; +use crate::query::structs::query::Query; use crate::query::tokenizer::Tokenizer; -use crate::query::Query; use query::validate_query_string; use std::env; use std::error::Error; @@ -30,5 +30,5 @@ fn run(args: Vec) -> Result, Box> { let query = Query::from(tokens)?; println!("\n{:?}", &query); let result = Executor::run(path, query)?; - Ok(result) + Ok(vec![]) } diff --git a/src/query/builder/delete.rs b/src/query/builder/delete.rs index 5ecce6d..a1b2663 100644 --- a/src/query/builder/delete.rs +++ b/src/query/builder/delete.rs @@ -1,8 +1,9 @@ use crate::query::builder::{validate_keywords, Builder}; use crate::query::errors::InvalidSQL; -use crate::query::Operation::Delete; -use crate::query::TokenKind::Keyword; -use crate::query::{Query, Token}; +use crate::query::structs::operation::Operation::Delete; +use crate::query::structs::query::Query; +use crate::query::structs::token::Token; +use crate::query::structs::token::TokenKind::Keyword; use std::collections::VecDeque; const ALLOWED_KEYWORDS: &[&str] = &["FROM", "WHERE", "AND", "OR"]; diff --git a/src/query/builder/expression.rs b/src/query/builder/expression.rs index f00f49e..9f0fe50 100644 --- a/src/query/builder/expression.rs +++ b/src/query/builder/expression.rs @@ -1,43 +1,17 @@ use crate::errored; -use crate::query::builder::expression::ExpressionNode::{Empty, Leaf}; -use crate::query::builder::expression::ExpressionOperator::{ - Equals, GreaterOrEqual, GreaterThan, LessOrEqual, LessThan, NotEquals, -}; use crate::query::errors::InvalidSQL; use crate::query::errors::InvalidSQL::Syntax; -use crate::query::TokenKind::Keyword; -use crate::query::{Token, TokenKind}; +use crate::query::structs::expression::ExpressionNode::{Empty, Leaf}; +use crate::query::structs::expression::ExpressionOperator::{ + Equals, GreaterOrEqual, GreaterThan, LessOrEqual, LessThan, NotEquals, +}; +use crate::query::structs::expression::{ExpressionNode, ExpressionOperator}; +use crate::query::structs::token::TokenKind::Keyword; +use crate::query::structs::token::{Token, TokenKind}; use std::collections::VecDeque; pub struct ExpressionBuilder; -#[derive(Debug, Default, PartialEq)] -pub enum ExpressionOperator { - #[default] - None, - Equals, - NotEquals, - GreaterThan, - LessThan, - GreaterOrEqual, - LessOrEqual, - And, - Or, - Not, -} - -#[derive(Default, PartialEq)] -pub enum ExpressionNode { - #[default] - Empty, - Leaf(Token), - Statement { - operator: ExpressionOperator, - left: Box, - right: Box, - }, -} - impl ExpressionBuilder { pub fn parse_expressions(tokens: &mut VecDeque) -> Result { ExpressionBuilder::parse_or(tokens) @@ -85,7 +59,7 @@ impl ExpressionBuilder { return Ok(ExpressionNode::Statement { operator: ExpressionOperator::Not, left: Box::new(node), - right: Box::new(ExpressionNode::Empty), + right: Box::new(Empty), }); } } diff --git a/src/query/builder/insert.rs b/src/query/builder/insert.rs index ed59a57..01cf0c5 100644 --- a/src/query/builder/insert.rs +++ b/src/query/builder/insert.rs @@ -1,10 +1,9 @@ -use crate::errored; use crate::query::builder::{unexpected_token_in_stage, validate_keywords, Builder}; use crate::query::errors::InvalidSQL; -use crate::query::errors::InvalidSQL::Syntax; -use crate::query::Operation::Insert; -use crate::query::TokenKind::{Keyword, ParenthesisClose, ParenthesisOpen}; -use crate::query::{Query, Token, TokenKind}; +use crate::query::structs::operation::Operation::Insert; +use crate::query::structs::query::Query; +use crate::query::structs::token::TokenKind::{Keyword, ParenthesisClose, ParenthesisOpen}; +use crate::query::structs::token::{Token, TokenKind}; use std::collections::VecDeque; const ALLOWED_KEYWORDS: &[&str] = &["VALUES"]; diff --git a/src/query/builder/mod.rs b/src/query/builder/mod.rs index 92e30c0..1bb4ad8 100644 --- a/src/query/builder/mod.rs +++ b/src/query/builder/mod.rs @@ -1,20 +1,21 @@ -mod delete; +pub mod delete; pub mod expression; -mod insert; -mod select; -mod update; +pub mod insert; +pub mod select; +pub mod update; use crate::errored; -use crate::query::builder::delete::DeleteBuilder; -use crate::query::builder::expression::{ExpressionBuilder, ExpressionNode}; -use crate::query::builder::insert::InsertBuilder; -use crate::query::builder::select::SelectBuilder; -use crate::query::builder::update::UpdateBuilder; +use crate::query::builder::expression::ExpressionBuilder; use crate::query::errors::InvalidSQL; use crate::query::errors::InvalidSQL::Syntax; -use crate::query::Operation::{Delete, Insert, Select, Unknown, Update}; -use crate::query::TokenKind::{Identifier, Keyword, Operator, ParenthesisClose, ParenthesisOpen}; -use crate::query::{Operation, Query, Token, TokenKind}; +use crate::query::structs::expression::ExpressionNode; +use crate::query::structs::operation::Operation; +use crate::query::structs::operation::Operation::{Delete, Insert, Select, Unknown, Update}; +use crate::query::structs::query::Query; +use crate::query::structs::token::TokenKind::{ + Identifier, Keyword, Operator, ParenthesisClose, ParenthesisOpen, +}; +use crate::query::structs::token::{Token, TokenKind}; use std::collections::VecDeque; pub trait Builder { @@ -101,21 +102,7 @@ pub trait Builder { fn validate_keywords(&self) -> Result<(), InvalidSQL>; } -impl Query { - pub fn from(tokens: Vec) -> Result { - let mut tokens = VecDeque::from(tokens); - let kind = get_kind(tokens.pop_front()); - match kind { - Unknown => errored!(Syntax, "query does not start with a valid operation."), - Select => SelectBuilder::new(tokens).build(), - Update => UpdateBuilder::new(tokens).build(), - Delete => DeleteBuilder::new(tokens).build(), - Insert => InsertBuilder::new(tokens).build(), - } - } -} - -fn get_kind(token: Option) -> Operation { +pub fn get_kind(token: Option) -> Operation { match token { Some(t) => match t.value.as_str() { "SELECT" => Select, diff --git a/src/query/builder/select.rs b/src/query/builder/select.rs index c93bad6..021dc68 100644 --- a/src/query/builder/select.rs +++ b/src/query/builder/select.rs @@ -1,9 +1,11 @@ use crate::query::builder::{unexpected_token_in_stage, validate_keywords, Builder}; use crate::query::errors::InvalidSQL; -use crate::query::Operation::Select; -use crate::query::OrderKind::{Asc, Desc}; -use crate::query::TokenKind::{Identifier, Keyword}; -use crate::query::{Ordering, Query, Token}; +use crate::query::structs::operation::Operation::Select; +use crate::query::structs::ordering::OrderKind::{Asc, Desc}; +use crate::query::structs::ordering::Ordering; +use crate::query::structs::query::Query; +use crate::query::structs::token::Token; +use crate::query::structs::token::TokenKind::{Identifier, Keyword}; use std::collections::VecDeque; const ALLOWED_KEYWORDS: &[&str] = &[ diff --git a/src/query/builder/update.rs b/src/query/builder/update.rs index b75fa32..b4266fc 100644 --- a/src/query/builder/update.rs +++ b/src/query/builder/update.rs @@ -1,11 +1,13 @@ use crate::errored; -use crate::query::builder::expression::{ExpressionBuilder, ExpressionNode}; -use crate::query::builder::{unexpected_token_in_stage, validate_keywords, Builder}; +use crate::query::builder::expression::ExpressionBuilder; +use crate::query::builder::{validate_keywords, Builder}; use crate::query::errors::InvalidSQL; use crate::query::errors::InvalidSQL::Syntax; -use crate::query::Operation::Update; -use crate::query::TokenKind::Keyword; -use crate::query::{Query, Token}; +use crate::query::structs::expression::ExpressionNode; +use crate::query::structs::operation::Operation::Update; +use crate::query::structs::query::Query; +use crate::query::structs::token::Token; +use crate::query::structs::token::TokenKind::Keyword; use std::collections::VecDeque; const ALLOWED_KEYWORDS: &[&str] = &["SET", "WHERE", "AND", "OR"]; diff --git a/src/query/executor/delete.rs b/src/query/executor/delete.rs index 8b13789..2ae4db9 100644 --- a/src/query/executor/delete.rs +++ b/src/query/executor/delete.rs @@ -1 +1,8 @@ +use crate::query::errors::InvalidSQL; +use crate::query::executor::Executor; +impl Executor { + pub fn run_delete() -> Result, InvalidSQL> { + todo!() + } +} diff --git a/src/query/executor/insert.rs b/src/query/executor/insert.rs index 8b13789..76466fe 100644 --- a/src/query/executor/insert.rs +++ b/src/query/executor/insert.rs @@ -1 +1,8 @@ +use crate::query::errors::InvalidSQL; +use crate::query::executor::Executor; +impl Executor { + pub fn run_insert() -> Result, InvalidSQL> { + todo!() + } +} diff --git a/src/query/executor/mod.rs b/src/query/executor/mod.rs index e2c8f17..66298d9 100644 --- a/src/query/executor/mod.rs +++ b/src/query/executor/mod.rs @@ -1,5 +1,6 @@ use crate::query::errors::InvalidSQL; -use crate::query::Query; +use crate::query::structs::operation::Operation::*; +use crate::query::structs::query::Query; mod delete; mod insert; @@ -10,6 +11,12 @@ pub struct Executor; impl Executor { pub fn run(path: &str, query: Query) -> Result, InvalidSQL> { - Ok(vec![]) + match query.operation { + Select => Executor::run_select(), + Update => Executor::run_update(), + Delete => Executor::run_delete(), + Insert => Executor::run_insert(), + _ => Ok(vec![]), + } } } diff --git a/src/query/executor/select.rs b/src/query/executor/select.rs index 8b13789..bc387aa 100644 --- a/src/query/executor/select.rs +++ b/src/query/executor/select.rs @@ -1 +1,8 @@ +use crate::query::errors::InvalidSQL; +use crate::query::executor::Executor; +impl Executor { + pub fn run_select() -> Result, InvalidSQL> { + todo!() + } +} diff --git a/src/query/executor/update.rs b/src/query/executor/update.rs index 8b13789..82d82ca 100644 --- a/src/query/executor/update.rs +++ b/src/query/executor/update.rs @@ -1 +1,8 @@ +use crate::query::errors::InvalidSQL; +use crate::query::executor::Executor; +impl Executor { + pub fn run_update() -> Result, InvalidSQL> { + todo!() + } +} diff --git a/src/query/mod.rs b/src/query/mod.rs index 2373781..57eef5f 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -1,139 +1,15 @@ pub mod builder; mod errors; pub mod executor; +pub mod structs; pub mod tokenizer; -use crate::query::builder::expression::ExpressionNode; -use crate::query::OrderKind::Asc; -use crate::query::TokenKind::Unknown; +use crate::errored; use crate::utils::errors::Errored; -use std::fmt::{Debug, Display, Formatter}; - -pub struct Query { - pub operation: Operation, - pub table: String, - columns: Vec, - inserts: Vec, - updates: Vec, - conditions: ExpressionNode, - ordering: Vec, -} - -#[derive(Debug, PartialEq)] -pub struct Token { - pub value: String, - pub kind: TokenKind, -} - -struct Ordering { - field: Token, - kind: OrderKind, -} - -#[derive(Debug, PartialEq)] -pub enum TokenKind { - Unknown, - String, - Number, - Operator, - Identifier, - ParenthesisOpen, - ParenthesisClose, - Keyword, -} - -#[derive(Debug, PartialEq)] -pub enum Operation { - Unknown, - Select, - Update, - Delete, - Insert, -} - -#[derive(Debug)] -enum OrderKind { - Asc, - Desc, -} - -impl Ordering { - pub fn default() -> Self { - Self { - field: Token::default(), - kind: Asc, - } - } -} - -impl Token { - pub fn default() -> Self { - Self { - value: String::new(), - kind: Unknown, - } - } -} - -impl Query { - pub fn default() -> Self { - Self { - operation: Operation::Unknown, - table: "".to_string(), - columns: vec![], - inserts: vec![], - updates: vec![], - conditions: ExpressionNode::default(), - ordering: vec![], - } - } -} - -impl Display for Query { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let fields: Vec<&str> = self.columns.iter().map(|f| f.value.as_str()).collect(); - let inserts: Vec<&str> = self.inserts.iter().map(|f| f.value.as_str()).collect(); - writeln!(f, "Query Kind: [{:?}]", self.operation)?; - writeln!(f, "Table: {:?}", self.table)?; - writeln!(f, "Columns: {:?}", fields)?; - writeln!(f, "Inserts: {:?}", inserts)?; - writeln!(f, "Updates: {:?}", self.updates)?; - writeln!(f, "Conditions: {:?}", self.conditions)?; - writeln!(f, "Ordering: {:?}", self.ordering) - } -} - -impl Debug for Query { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", &self) - } -} - -impl Debug for Ordering { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "({}:{:?})", &self.field.value, &self.kind) - } -} - -impl Debug for ExpressionNode { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - ExpressionNode::Empty => write!(f, "()"), - ExpressionNode::Leaf(t) => write!(f, "{}", t.value), - ExpressionNode::Statement { - operator, - left, - right, - } => { - write!(f, "{:?}[{:?},{:?}]", operator, left, right) - } - } - } -} pub fn validate_query_string(query: &str) -> Result<(), Errored> { if query.trim().is_empty() { - return Err(Errored(String::from("query is empty."))); + errored!(Errored, "query is empty."); } Ok(()) } diff --git a/src/query/structs/expression.rs b/src/query/structs/expression.rs new file mode 100644 index 0000000..aab80ba --- /dev/null +++ b/src/query/structs/expression.rs @@ -0,0 +1,45 @@ +use crate::query::structs::token::Token; +use std::fmt::{Debug, Formatter}; + +#[derive(Default, PartialEq)] +pub enum ExpressionNode { + #[default] + Empty, + Leaf(Token), + Statement { + operator: ExpressionOperator, + left: Box, + right: Box, + }, +} + +#[derive(Debug, Default, PartialEq)] +pub enum ExpressionOperator { + #[default] + None, + Equals, + NotEquals, + GreaterThan, + LessThan, + GreaterOrEqual, + LessOrEqual, + And, + Or, + Not, +} + +impl Debug for ExpressionNode { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ExpressionNode::Empty => write!(f, "()"), + ExpressionNode::Leaf(t) => write!(f, "{}", t.value), + ExpressionNode::Statement { + operator, + left, + right, + } => { + write!(f, "{:?}[{:?},{:?}]", operator, left, right) + } + } + } +} diff --git a/src/query/structs/mod.rs b/src/query/structs/mod.rs new file mode 100644 index 0000000..c74eca9 --- /dev/null +++ b/src/query/structs/mod.rs @@ -0,0 +1,5 @@ +pub mod expression; +pub mod operation; +pub mod ordering; +pub mod query; +pub mod token; diff --git a/src/query/structs/operation.rs b/src/query/structs/operation.rs new file mode 100644 index 0000000..bb50f60 --- /dev/null +++ b/src/query/structs/operation.rs @@ -0,0 +1,8 @@ +#[derive(Debug, PartialEq)] +pub enum Operation { + Unknown, + Select, + Update, + Delete, + Insert, +} diff --git a/src/query/structs/ordering.rs b/src/query/structs/ordering.rs new file mode 100644 index 0000000..e6ec10b --- /dev/null +++ b/src/query/structs/ordering.rs @@ -0,0 +1,29 @@ +use crate::query::structs::ordering::OrderKind::Asc; +use crate::query::structs::token::Token; +use std::fmt::{Debug, Formatter}; + +pub struct Ordering { + pub(crate) field: Token, + pub(crate) kind: OrderKind, +} + +#[derive(Debug)] +pub enum OrderKind { + Asc, + Desc, +} + +impl Ordering { + pub fn default() -> Self { + Self { + field: Token::default(), + kind: Asc, + } + } +} + +impl Debug for Ordering { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "({}:{:?})", &self.field.value, &self.kind) + } +} diff --git a/src/query/structs/query.rs b/src/query/structs/query.rs new file mode 100644 index 0000000..f806829 --- /dev/null +++ b/src/query/structs/query.rs @@ -0,0 +1,71 @@ +use crate::errored; +use crate::query::builder::delete::DeleteBuilder; +use crate::query::builder::insert::InsertBuilder; +use crate::query::builder::select::SelectBuilder; +use crate::query::builder::update::UpdateBuilder; +use crate::query::builder::{get_kind, Builder}; +use crate::query::errors::InvalidSQL; +use crate::query::errors::InvalidSQL::Syntax; +use crate::query::structs::expression::ExpressionNode; +use crate::query::structs::operation::Operation; +use crate::query::structs::operation::Operation::{Delete, Insert, Select, Unknown, Update}; +use crate::query::structs::ordering::Ordering; +use crate::query::structs::token::Token; +use std::collections::VecDeque; +use std::fmt::{Debug, Display, Formatter}; + +pub struct Query { + pub operation: Operation, + pub table: String, + pub(crate) columns: Vec, + pub(crate) inserts: Vec, + pub(crate) updates: Vec, + pub(crate) conditions: ExpressionNode, + pub(crate) ordering: Vec, +} + +impl Query { + pub fn default() -> Self { + Self { + operation: Unknown, + table: "".to_string(), + columns: vec![], + inserts: vec![], + updates: vec![], + conditions: ExpressionNode::default(), + ordering: vec![], + } + } + + pub fn from(tokens: Vec) -> Result { + let mut tokens = VecDeque::from(tokens); + let kind = get_kind(tokens.pop_front()); + match kind { + Unknown => errored!(Syntax, "query does not start with a valid operation."), + Select => SelectBuilder::new(tokens).build(), + Update => UpdateBuilder::new(tokens).build(), + Delete => DeleteBuilder::new(tokens).build(), + Insert => InsertBuilder::new(tokens).build(), + } + } +} + +impl Display for Query { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let fields: Vec<&str> = self.columns.iter().map(|f| f.value.as_str()).collect(); + let inserts: Vec<&str> = self.inserts.iter().map(|f| f.value.as_str()).collect(); + writeln!(f, "Query Kind: [{:?}]", self.operation)?; + writeln!(f, "Table: {:?}", self.table)?; + writeln!(f, "Columns: {:?}", fields)?; + writeln!(f, "Inserts: {:?}", inserts)?; + writeln!(f, "Updates: {:?}", self.updates)?; + writeln!(f, "Conditions: {:?}", self.conditions)?; + writeln!(f, "Ordering: {:?}", self.ordering) + } +} + +impl Debug for Query { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self) + } +} diff --git a/src/query/structs/token.rs b/src/query/structs/token.rs new file mode 100644 index 0000000..969eba3 --- /dev/null +++ b/src/query/structs/token.rs @@ -0,0 +1,28 @@ +use crate::query::structs::token::TokenKind::Unknown; + +#[derive(Debug, PartialEq)] +pub struct Token { + pub value: String, + pub kind: TokenKind, +} + +#[derive(Debug, PartialEq)] +pub enum TokenKind { + Unknown, + String, + Number, + Operator, + Identifier, + ParenthesisOpen, + ParenthesisClose, + Keyword, +} + +impl Token { + pub fn default() -> Self { + Self { + value: String::new(), + kind: Unknown, + } + } +} diff --git a/src/query/tokenizer.rs b/src/query/tokenizer.rs index c4a4878..0579c2c 100644 --- a/src/query/tokenizer.rs +++ b/src/query/tokenizer.rs @@ -1,11 +1,11 @@ use crate::errored; use crate::query::errors::InvalidSQL; use crate::query::errors::InvalidSQL::Syntax; -use crate::query::tokenizer::TokenizerState::*; -use crate::query::TokenKind::{ +use crate::query::structs::token::TokenKind::{ Identifier, Keyword, Number, ParenthesisClose, ParenthesisOpen, Unknown, }; -use crate::query::{Token, TokenKind}; +use crate::query::structs::token::{Token, TokenKind}; +use crate::query::tokenizer::TokenizerState::*; const VALID_OPERATORS: &[&str] = &["*", "=", "<", ">", "!", ">=", "<=", "!="]; From d98caae91979268894c730f6b1915d22497dbc43 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sat, 7 Sep 2024 23:14:54 -0300 Subject: [PATCH 044/103] error simplification and file utils added --- src/main.rs | 8 ++++--- src/query/builder/delete.rs | 6 ++--- src/query/builder/expression.rs | 30 +++++++++++-------------- src/query/builder/insert.rs | 8 +++---- src/query/builder/mod.rs | 24 ++++++++++---------- src/query/builder/select.rs | 8 +++---- src/query/builder/update.rs | 10 ++++----- src/query/errors.rs | 39 --------------------------------- src/query/executor/delete.rs | 5 +++-- src/query/executor/insert.rs | 5 +++-- src/query/executor/mod.rs | 33 ++++++++++++++++++++-------- src/query/executor/select.rs | 7 +++--- src/query/executor/update.rs | 5 +++-- src/query/mod.rs | 4 ++-- src/query/structs/query.rs | 16 +++++++------- src/query/tokenizer.rs | 20 ++++++++--------- src/utils/errors.rs | 25 ++++++++++++++++++--- src/utils/files.rs | 32 ++++++++++++++++++++++++--- src/utils/mod.rs | 2 +- tests/person.csv | 11 ++++++++++ 20 files changed, 166 insertions(+), 132 deletions(-) delete mode 100644 src/query/errors.rs create mode 100644 tests/person.csv diff --git a/src/main.rs b/src/main.rs index f3efd9c..e85ace2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use crate::query::executor::Executor; use crate::query::structs::query::Query; use crate::query::tokenizer::Tokenizer; +use crate::utils::files::validate_path; use query::validate_query_string; use std::env; use std::error::Error; @@ -16,7 +17,7 @@ fn main() -> Result<(), Box> { Ok(()) } -fn run(args: Vec) -> Result, Box> { +fn run(args: Vec) -> Result<(), Box> { if args.len() != 3 { println!("invalid usage of rustic-sql"); return Err("usage: cargo run -- ".into()); @@ -24,11 +25,12 @@ fn run(args: Vec) -> Result, Box> { let path: &String = &args[1]; let query: &String = &args[2]; + validate_path(path)?; validate_query_string(query)?; let tokens = Tokenizer::new().tokenize(query)?; let query = Query::from(tokens)?; println!("\n{:?}", &query); - let result = Executor::run(path, query)?; - Ok(vec![]) + Executor::run(path, query)?; + Ok(()) } diff --git a/src/query/builder/delete.rs b/src/query/builder/delete.rs index a1b2663..2d0ace0 100644 --- a/src/query/builder/delete.rs +++ b/src/query/builder/delete.rs @@ -1,9 +1,9 @@ use crate::query::builder::{validate_keywords, Builder}; -use crate::query::errors::InvalidSQL; use crate::query::structs::operation::Operation::Delete; use crate::query::structs::query::Query; use crate::query::structs::token::Token; use crate::query::structs::token::TokenKind::Keyword; +use crate::utils::errors::Errored; use std::collections::VecDeque; const ALLOWED_KEYWORDS: &[&str] = &["FROM", "WHERE", "AND", "OR"]; @@ -19,7 +19,7 @@ impl DeleteBuilder { } impl Builder for DeleteBuilder { - fn build(&mut self) -> Result { + fn build(&mut self) -> Result { let mut query = Query::default(); self.validate_keywords()?; query.operation = Delete; @@ -37,7 +37,7 @@ impl Builder for DeleteBuilder { &mut self.tokens } - fn validate_keywords(&self) -> Result<(), InvalidSQL> { + fn validate_keywords(&self) -> Result<(), Errored> { validate_keywords(ALLOWED_KEYWORDS, &self.tokens, Delete) } } diff --git a/src/query/builder/expression.rs b/src/query/builder/expression.rs index 9f0fe50..f8e075f 100644 --- a/src/query/builder/expression.rs +++ b/src/query/builder/expression.rs @@ -1,23 +1,21 @@ use crate::errored; -use crate::query::errors::InvalidSQL; -use crate::query::errors::InvalidSQL::Syntax; use crate::query::structs::expression::ExpressionNode::{Empty, Leaf}; -use crate::query::structs::expression::ExpressionOperator::{ - Equals, GreaterOrEqual, GreaterThan, LessOrEqual, LessThan, NotEquals, -}; +use crate::query::structs::expression::ExpressionOperator::*; use crate::query::structs::expression::{ExpressionNode, ExpressionOperator}; use crate::query::structs::token::TokenKind::Keyword; use crate::query::structs::token::{Token, TokenKind}; +use crate::utils::errors::Errored; +use crate::utils::errors::Errored::Syntax; use std::collections::VecDeque; pub struct ExpressionBuilder; impl ExpressionBuilder { - pub fn parse_expressions(tokens: &mut VecDeque) -> Result { + pub fn parse_expressions(tokens: &mut VecDeque) -> Result { ExpressionBuilder::parse_or(tokens) } - fn parse_or(tokens: &mut VecDeque) -> Result { + fn parse_or(tokens: &mut VecDeque) -> Result { let mut left = ExpressionBuilder::parse_and(tokens)?; while let Some(t) = tokens.front() { if t.kind != Keyword || t.value != "OR" { @@ -26,7 +24,7 @@ impl ExpressionBuilder { tokens.pop_front(); let right = ExpressionBuilder::parse_and(tokens)?; left = ExpressionNode::Statement { - operator: ExpressionOperator::Or, + operator: Or, left: Box::new(left), right: Box::new(right), }; @@ -34,7 +32,7 @@ impl ExpressionBuilder { Ok(left) } - fn parse_and(tokens: &mut VecDeque) -> Result { + fn parse_and(tokens: &mut VecDeque) -> Result { let mut left = ExpressionBuilder::parse_not(tokens)?; while let Some(t) = tokens.front() { if t.kind != Keyword || t.value != "AND" { @@ -43,7 +41,7 @@ impl ExpressionBuilder { tokens.pop_front(); let right = ExpressionBuilder::parse_not(tokens)?; left = ExpressionNode::Statement { - operator: ExpressionOperator::And, + operator: And, left: Box::new(left), right: Box::new(right), }; @@ -51,13 +49,13 @@ impl ExpressionBuilder { Ok(left) } - fn parse_not(tokens: &mut VecDeque) -> Result { + fn parse_not(tokens: &mut VecDeque) -> Result { if let Some(t) = tokens.front() { if t.kind == Keyword && t.value == "NOT" { tokens.pop_front(); let node = ExpressionBuilder::parse_comparisons(tokens)?; return Ok(ExpressionNode::Statement { - operator: ExpressionOperator::Not, + operator: Not, left: Box::new(node), right: Box::new(Empty), }); @@ -66,7 +64,7 @@ impl ExpressionBuilder { ExpressionBuilder::parse_comparisons(tokens) } - fn parse_comparisons(tokens: &mut VecDeque) -> Result { + fn parse_comparisons(tokens: &mut VecDeque) -> Result { let left = ExpressionBuilder::parse_leaf(tokens)?; let operator = ExpressionBuilder::parse_simple_operator(tokens); if operator.is_err() { @@ -80,7 +78,7 @@ impl ExpressionBuilder { }) } - fn parse_leaf(tokens: &mut VecDeque) -> Result { + fn parse_leaf(tokens: &mut VecDeque) -> Result { let mut leaf = Empty; while let Some(t) = tokens.front() { match t.kind { @@ -104,9 +102,7 @@ impl ExpressionBuilder { Ok(leaf) } - fn parse_simple_operator( - tokens: &mut VecDeque, - ) -> Result { + fn parse_simple_operator(tokens: &mut VecDeque) -> Result { let t = tokens .front() .ok_or_else(|| Syntax("expected operator but was end of query.".to_string()))?; diff --git a/src/query/builder/insert.rs b/src/query/builder/insert.rs index 01cf0c5..66ffd77 100644 --- a/src/query/builder/insert.rs +++ b/src/query/builder/insert.rs @@ -1,9 +1,9 @@ use crate::query::builder::{unexpected_token_in_stage, validate_keywords, Builder}; -use crate::query::errors::InvalidSQL; use crate::query::structs::operation::Operation::Insert; use crate::query::structs::query::Query; use crate::query::structs::token::TokenKind::{Keyword, ParenthesisClose, ParenthesisOpen}; use crate::query::structs::token::{Token, TokenKind}; +use crate::utils::errors::Errored; use std::collections::VecDeque; const ALLOWED_KEYWORDS: &[&str] = &["VALUES"]; @@ -17,7 +17,7 @@ impl InsertBuilder { Self { tokens } } - fn parse_insert_values(&mut self) -> Result, InvalidSQL> { + fn parse_insert_values(&mut self) -> Result, Errored> { self.pop_expecting("VALUES", Keyword)?; self.peek_expecting("(", ParenthesisOpen)?; let mut values = vec![]; @@ -42,7 +42,7 @@ impl InsertBuilder { } impl Builder for InsertBuilder { - fn build(&mut self) -> Result { + fn build(&mut self) -> Result { let mut query = Query::default(); self.validate_keywords()?; query.operation = Insert; @@ -58,7 +58,7 @@ impl Builder for InsertBuilder { &mut self.tokens } - fn validate_keywords(&self) -> Result<(), InvalidSQL> { + fn validate_keywords(&self) -> Result<(), Errored> { validate_keywords(ALLOWED_KEYWORDS, &self.tokens, Insert) } } diff --git a/src/query/builder/mod.rs b/src/query/builder/mod.rs index 1bb4ad8..03bff65 100644 --- a/src/query/builder/mod.rs +++ b/src/query/builder/mod.rs @@ -6,8 +6,6 @@ pub mod update; use crate::errored; use crate::query::builder::expression::ExpressionBuilder; -use crate::query::errors::InvalidSQL; -use crate::query::errors::InvalidSQL::Syntax; use crate::query::structs::expression::ExpressionNode; use crate::query::structs::operation::Operation; use crate::query::structs::operation::Operation::{Delete, Insert, Select, Unknown, Update}; @@ -16,13 +14,15 @@ use crate::query::structs::token::TokenKind::{ Identifier, Keyword, Operator, ParenthesisClose, ParenthesisOpen, }; use crate::query::structs::token::{Token, TokenKind}; +use crate::utils::errors::Errored; +use crate::utils::errors::Errored::*; use std::collections::VecDeque; pub trait Builder { - fn build(&mut self) -> Result; + fn build(&mut self) -> Result; fn tokens(&mut self) -> &mut VecDeque; - fn parse_table(&mut self, operation: Operation) -> Result { + fn parse_table(&mut self, operation: Operation) -> Result { if let Some(t) = self.tokens().front() { match operation { Select | Delete => { @@ -42,7 +42,7 @@ pub trait Builder { Ok(t.value) } - fn parse_columns(&mut self) -> Result, InvalidSQL> { + fn parse_columns(&mut self) -> Result, Errored> { let mut fields: Vec = vec![]; while let Some(t) = self.tokens().front() { match t.kind { @@ -67,24 +67,24 @@ pub trait Builder { Ok(fields) } - fn parse_where(&mut self) -> Result { + fn parse_where(&mut self) -> Result { self.pop_expecting("WHERE", Keyword)?; ExpressionBuilder::parse_expressions(self.tokens()) } - fn expect_none(&mut self) -> Result<(), InvalidSQL> { + fn expect_none(&mut self) -> Result<(), Errored> { if let Some(t) = self.tokens().front() { errored!(Syntax, "expected end of query but got: {:?}", t); } Ok(()) } - fn pop_expecting(&mut self, value: &str, kind: TokenKind) -> Result, InvalidSQL> { + fn pop_expecting(&mut self, value: &str, kind: TokenKind) -> Result, Errored> { self.peek_expecting(value, kind)?; Ok(self.tokens().pop_front()) } - fn peek_expecting(&mut self, value: &str, kind: TokenKind) -> Result<(), InvalidSQL> { + fn peek_expecting(&mut self, value: &str, kind: TokenKind) -> Result<(), Errored> { let expected = Token { value: value.to_string(), kind, @@ -99,7 +99,7 @@ pub trait Builder { Ok(()) } - fn validate_keywords(&self) -> Result<(), InvalidSQL>; + fn validate_keywords(&self) -> Result<(), Errored>; } pub fn get_kind(token: Option) -> Operation { @@ -119,7 +119,7 @@ fn validate_keywords( allowed: &[&str], tokens: &VecDeque, operation: Operation, -) -> Result<(), InvalidSQL> { +) -> Result<(), Errored> { let keywords: VecDeque<&Token> = tokens.iter().filter(|t| t.kind == Keyword).collect(); for word in keywords { if !allowed.contains(&&*word.value) { @@ -134,7 +134,7 @@ fn validate_keywords( Ok(()) } -pub fn unexpected_token_in_stage(stage: &str, token: &Token) -> Result<(), InvalidSQL> { +pub fn unexpected_token_in_stage(stage: &str, token: &Token) -> Result<(), Errored> { errored!( Syntax, "unexpected token while parsing {} fields: {:?}", diff --git a/src/query/builder/select.rs b/src/query/builder/select.rs index 021dc68..c9a20be 100644 --- a/src/query/builder/select.rs +++ b/src/query/builder/select.rs @@ -1,11 +1,11 @@ use crate::query::builder::{unexpected_token_in_stage, validate_keywords, Builder}; -use crate::query::errors::InvalidSQL; use crate::query::structs::operation::Operation::Select; use crate::query::structs::ordering::OrderKind::{Asc, Desc}; use crate::query::structs::ordering::Ordering; use crate::query::structs::query::Query; use crate::query::structs::token::Token; use crate::query::structs::token::TokenKind::{Identifier, Keyword}; +use crate::utils::errors::Errored; use std::collections::VecDeque; const ALLOWED_KEYWORDS: &[&str] = &[ @@ -21,7 +21,7 @@ impl SelectBuilder { Self { tokens } } - fn parse_ordering(&mut self) -> Result, InvalidSQL> { + fn parse_ordering(&mut self) -> Result, Errored> { let mut ordering = vec![]; while let Some(t) = self.tokens.pop_front() { if t.kind != Identifier { @@ -45,7 +45,7 @@ impl SelectBuilder { } impl Builder for SelectBuilder { - fn build(&mut self) -> Result { + fn build(&mut self) -> Result { let mut query = Query::default(); self.validate_keywords()?; query.operation = Select; @@ -68,7 +68,7 @@ impl Builder for SelectBuilder { &mut self.tokens } - fn validate_keywords(&self) -> Result<(), InvalidSQL> { + fn validate_keywords(&self) -> Result<(), Errored> { validate_keywords(ALLOWED_KEYWORDS, &self.tokens, Select) } } diff --git a/src/query/builder/update.rs b/src/query/builder/update.rs index b4266fc..03a7829 100644 --- a/src/query/builder/update.rs +++ b/src/query/builder/update.rs @@ -1,13 +1,13 @@ use crate::errored; use crate::query::builder::expression::ExpressionBuilder; use crate::query::builder::{validate_keywords, Builder}; -use crate::query::errors::InvalidSQL; -use crate::query::errors::InvalidSQL::Syntax; use crate::query::structs::expression::ExpressionNode; use crate::query::structs::operation::Operation::Update; use crate::query::structs::query::Query; use crate::query::structs::token::Token; use crate::query::structs::token::TokenKind::Keyword; +use crate::utils::errors::Errored; +use crate::utils::errors::Errored::Syntax; use std::collections::VecDeque; const ALLOWED_KEYWORDS: &[&str] = &["SET", "WHERE", "AND", "OR"]; @@ -21,7 +21,7 @@ impl UpdateBuilder { Self { tokens } } - fn parse_updates(&mut self) -> Result, InvalidSQL> { + fn parse_updates(&mut self) -> Result, Errored> { self.pop_expecting("SET", Keyword)?; let mut updates = vec![]; while let Some(t) = self.tokens.front() { @@ -44,7 +44,7 @@ impl UpdateBuilder { } impl Builder for UpdateBuilder { - fn build(&mut self) -> Result { + fn build(&mut self) -> Result { let mut query = Query::default(); self.validate_keywords()?; query.operation = Update; @@ -63,7 +63,7 @@ impl Builder for UpdateBuilder { &mut self.tokens } - fn validate_keywords(&self) -> Result<(), InvalidSQL> { + fn validate_keywords(&self) -> Result<(), Errored> { validate_keywords(ALLOWED_KEYWORDS, &self.tokens, Update) } } diff --git a/src/query/errors.rs b/src/query/errors.rs deleted file mode 100644 index 22c0bd4..0000000 --- a/src/query/errors.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::error::Error; -use std::fmt::{Debug, Display, Formatter}; - -pub enum InvalidSQL { - Syntax(String), - Column(String, String), - Table(String, String), -} - -impl Error for InvalidSQL {} -impl Debug for InvalidSQL { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self) - } -} - -impl Display for InvalidSQL { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - InvalidSQL::Syntax(syntax) => { - write!(f, "[INVALID_SYNTAX]: {}", syntax) - } - InvalidSQL::Column(column, details) => { - write!( - f, - "[INVALID_COLUMN]: column {} is invalid because {}.", - column, details - ) - } - InvalidSQL::Table(table, details) => { - write!( - f, - "[INVALID_TABLE]: table {} is invalid because {}.", - table, details - ) - } - } - } -} diff --git a/src/query/executor/delete.rs b/src/query/executor/delete.rs index 2ae4db9..26b2b21 100644 --- a/src/query/executor/delete.rs +++ b/src/query/executor/delete.rs @@ -1,8 +1,9 @@ -use crate::query::errors::InvalidSQL; use crate::query::executor::Executor; +use crate::utils::errors::Errored; +use std::fs::File; impl Executor { - pub fn run_delete() -> Result, InvalidSQL> { + pub fn run_delete(&self, table: File) -> Result<(), Errored> { todo!() } } diff --git a/src/query/executor/insert.rs b/src/query/executor/insert.rs index 76466fe..d2ec977 100644 --- a/src/query/executor/insert.rs +++ b/src/query/executor/insert.rs @@ -1,8 +1,9 @@ -use crate::query::errors::InvalidSQL; use crate::query::executor::Executor; +use crate::utils::errors::Errored; +use std::fs::File; impl Executor { - pub fn run_insert() -> Result, InvalidSQL> { + pub fn run_insert(&self, table: File) -> Result<(), Errored> { todo!() } } diff --git a/src/query/executor/mod.rs b/src/query/executor/mod.rs index 66298d9..f7febe2 100644 --- a/src/query/executor/mod.rs +++ b/src/query/executor/mod.rs @@ -1,22 +1,37 @@ -use crate::query::errors::InvalidSQL; +use crate::errored; use crate::query::structs::operation::Operation::*; use crate::query::structs::query::Query; +use crate::utils::errors::Errored; +use crate::utils::errors::Errored::Syntax; +use crate::utils::files::get_table_file; mod delete; mod insert; mod select; mod update; -pub struct Executor; +pub struct Executor { + path: String, + query: Query, +} impl Executor { - pub fn run(path: &str, query: Query) -> Result, InvalidSQL> { - match query.operation { - Select => Executor::run_select(), - Update => Executor::run_update(), - Delete => Executor::run_delete(), - Insert => Executor::run_insert(), - _ => Ok(vec![]), + pub fn new(path: &str, query: Query) -> Self { + Executor { + path: path.to_string(), + query, + } + } + + pub fn run(path: &str, query: Query) -> Result<(), Errored> { + let executor = Executor::new(path, query); + let table = get_table_file(&executor.path, &executor.query.table)?; + match executor.query.operation { + Select => executor.run_select(table), + Update => executor.run_update(table), + Delete => executor.run_delete(table), + Insert => executor.run_insert(table), + _ => errored!(Syntax, "unknown operation trying to be executed."), } } } diff --git a/src/query/executor/select.rs b/src/query/executor/select.rs index bc387aa..4f74a0d 100644 --- a/src/query/executor/select.rs +++ b/src/query/executor/select.rs @@ -1,8 +1,9 @@ -use crate::query::errors::InvalidSQL; use crate::query::executor::Executor; +use crate::utils::errors::Errored; +use std::fs::File; impl Executor { - pub fn run_select() -> Result, InvalidSQL> { - todo!() + pub fn run_select(&self, table: File) -> Result<(), Errored> { + Ok(()) } } diff --git a/src/query/executor/update.rs b/src/query/executor/update.rs index 82d82ca..37b1d4e 100644 --- a/src/query/executor/update.rs +++ b/src/query/executor/update.rs @@ -1,8 +1,9 @@ -use crate::query::errors::InvalidSQL; use crate::query::executor::Executor; +use crate::utils::errors::Errored; +use std::fs::File; impl Executor { - pub fn run_update() -> Result, InvalidSQL> { + pub fn run_update(&self, table: File) -> Result<(), Errored> { todo!() } } diff --git a/src/query/mod.rs b/src/query/mod.rs index 57eef5f..7fe44db 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -1,15 +1,15 @@ pub mod builder; -mod errors; pub mod executor; pub mod structs; pub mod tokenizer; use crate::errored; use crate::utils::errors::Errored; +use crate::utils::errors::Errored::Default; pub fn validate_query_string(query: &str) -> Result<(), Errored> { if query.trim().is_empty() { - errored!(Errored, "query is empty."); + errored!(Default, "query is empty."); } Ok(()) } diff --git a/src/query/structs/query.rs b/src/query/structs/query.rs index f806829..8cd9a2c 100644 --- a/src/query/structs/query.rs +++ b/src/query/structs/query.rs @@ -4,24 +4,24 @@ use crate::query::builder::insert::InsertBuilder; use crate::query::builder::select::SelectBuilder; use crate::query::builder::update::UpdateBuilder; use crate::query::builder::{get_kind, Builder}; -use crate::query::errors::InvalidSQL; -use crate::query::errors::InvalidSQL::Syntax; use crate::query::structs::expression::ExpressionNode; use crate::query::structs::operation::Operation; use crate::query::structs::operation::Operation::{Delete, Insert, Select, Unknown, Update}; use crate::query::structs::ordering::Ordering; use crate::query::structs::token::Token; +use crate::utils::errors::Errored; +use crate::utils::errors::Errored::Syntax; use std::collections::VecDeque; use std::fmt::{Debug, Display, Formatter}; pub struct Query { pub operation: Operation, pub table: String, - pub(crate) columns: Vec, - pub(crate) inserts: Vec, - pub(crate) updates: Vec, - pub(crate) conditions: ExpressionNode, - pub(crate) ordering: Vec, + pub columns: Vec, + pub inserts: Vec, + pub updates: Vec, + pub conditions: ExpressionNode, + pub ordering: Vec, } impl Query { @@ -37,7 +37,7 @@ impl Query { } } - pub fn from(tokens: Vec) -> Result { + pub fn from(tokens: Vec) -> Result { let mut tokens = VecDeque::from(tokens); let kind = get_kind(tokens.pop_front()); match kind { diff --git a/src/query/tokenizer.rs b/src/query/tokenizer.rs index 0579c2c..030ba36 100644 --- a/src/query/tokenizer.rs +++ b/src/query/tokenizer.rs @@ -1,11 +1,11 @@ use crate::errored; -use crate::query::errors::InvalidSQL; -use crate::query::errors::InvalidSQL::Syntax; use crate::query::structs::token::TokenKind::{ Identifier, Keyword, Number, ParenthesisClose, ParenthesisOpen, Unknown, }; use crate::query::structs::token::{Token, TokenKind}; use crate::query::tokenizer::TokenizerState::*; +use crate::utils::errors::Errored; +use crate::utils::errors::Errored::Syntax; const VALID_OPERATORS: &[&str] = &["*", "=", "<", ">", "!", ">=", "<=", "!="]; @@ -54,7 +54,7 @@ impl Tokenizer { } } - pub fn tokenize(&mut self, sql: &str) -> Result, InvalidSQL> { + pub fn tokenize(&mut self, sql: &str) -> Result, Errored> { let mut out = vec![]; let mut token = Token::default(); while self.i < sql.len() { @@ -85,7 +85,7 @@ impl Tokenizer { Ok(out) } - fn next_state(&mut self, c: char) -> Result<(), InvalidSQL> { + fn next_state(&mut self, c: char) -> Result<(), Errored> { match c { c if can_be_skipped(c) => self.i += c.len_utf8(), c if c.is_ascii_digit() => self.state = NumberLiteral, @@ -104,7 +104,7 @@ impl Tokenizer { Ok(()) } - fn tokenize_parenthesis(&mut self, sql: &str) -> Result { + fn tokenize_parenthesis(&mut self, sql: &str) -> Result { let c = char_at(self.i, sql); let mut token = Token::default(); if c == '(' { @@ -123,7 +123,7 @@ impl Tokenizer { Ok(token) } - fn tokenize_identifier_or_keyword(&mut self, sql: &str) -> Result { + fn tokenize_identifier_or_keyword(&mut self, sql: &str) -> Result { if let Some(word) = self.matches_keyword(sql) { self.i += word.len(); self.state = Complete; @@ -135,11 +135,11 @@ impl Tokenizer { self.tokenize_kind(sql, Identifier, is_identifier_char) } - fn tokenize_number(&mut self, sql: &str) -> Result { + fn tokenize_number(&mut self, sql: &str) -> Result { self.tokenize_kind(sql, Number, |c| c.is_ascii_digit()) } - fn tokenize_operator(&mut self, sql: &str) -> Result { + fn tokenize_operator(&mut self, sql: &str) -> Result { if let Some(op) = self.matches_operator(sql) { self.i += op.len(); self.state = Complete; @@ -157,7 +157,7 @@ impl Tokenizer { } } - fn tokenize_quoted(&mut self, sql: &str) -> Result { + fn tokenize_quoted(&mut self, sql: &str) -> Result { let start = self.i; for (index, char) in sql[start..].char_indices() { if char == '\'' { @@ -209,7 +209,7 @@ impl Tokenizer { sql: &str, output_kind: TokenKind, matches_kind: F, - ) -> Result + ) -> Result where F: Fn(char) -> bool, { diff --git a/src/utils/errors.rs b/src/utils/errors.rs index 7633b8b..48ad1ca 100644 --- a/src/utils/errors.rs +++ b/src/utils/errors.rs @@ -1,3 +1,4 @@ +use crate::utils::errors::Errored::*; use std::error::Error; use std::fmt::{Debug, Display, Formatter}; use std::io; @@ -13,7 +14,12 @@ macro_rules! errored { } /// Generic Error for the RusticSQL Application. -pub struct Errored(pub String); +pub enum Errored { + Syntax(String), + Column(String), + Table(String), + Default(String), +} impl Error for Errored {} @@ -25,12 +31,25 @@ impl Debug for Errored { impl Display for Errored { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "[ERROR]: {}", self.0) + match self { + Syntax(syntax) => { + write!(f, "[INVALID_SYNTAX]: {}", syntax) + } + Column(column) => { + write!(f, "[INVALID_COLUMN]: {}", column) + } + Table(table) => { + write!(f, "[INVALID_TABLE]: {}", table) + } + Default(error) => { + write!(f, "[ERROR]: {}", error) + } + } } } impl From for Errored { fn from(value: io::Error) -> Self { - Errored(format!("IO Error: {}", value)) + Default(format!("IO Error: {}", value)) } } diff --git a/src/utils/files.rs b/src/utils/files.rs index e3abb93..3dbc3cb 100644 --- a/src/utils/files.rs +++ b/src/utils/files.rs @@ -1,15 +1,41 @@ use crate::errored; use crate::utils::errors::Errored; +use crate::utils::errors::Errored::{Default, Table}; +use std::fs::File; use std::path::Path; +const CSV_EXTENSION: &str = ".csv"; + pub fn validate_path(dir: &str) -> Result<&Path, Errored> { let path = Path::new(dir); if !path.exists() { - errored!(Errored, "path '{dir}' does not exist"); + errored!(Default, "path '{dir}' does not exist"); } else if !path.is_dir() { - errored!(Errored, "path '{dir}' is not a valid directory"); + errored!(Default, "path '{dir}' is not a valid directory"); } else if path.read_dir()?.next().is_none() { - errored!(Errored, "path '{dir}' is an empty directory"); + errored!(Default, "path '{dir}' is an empty directory"); } Ok(path) } + +pub fn get_table_file(dir_path: &str, table_name: &str) -> Result { + let path = Path::new(dir_path); + let table_path = path.join(format!("{}{}", table_name, CSV_EXTENSION)); + if !table_path.is_file() { + errored!( + Default, + "table {} does not exist in directory: {}", + table_name, + dir_path + ); + } + match File::open(table_path) { + Ok(f) => Ok(f), + Err(err) => errored!( + Table, + "could not read table {} file, cause: {}", + table_name, + err + ), + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index ca605cf..cb26b62 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,2 +1,2 @@ pub mod errors; -mod files; +pub mod files; diff --git a/tests/person.csv b/tests/person.csv new file mode 100644 index 0000000..94a0d3c --- /dev/null +++ b/tests/person.csv @@ -0,0 +1,11 @@ +Nombre,Apellido,Edad,Correo electronico,Profesion +Juan,Perez,32,jperez@gmail.com,medico +Maria,Gomez,28,mgomez@gmail.com,abogado +Carlos,Sánchez,45,csanchez@gmail.com,ingeniero +Ana,Ruiz,36,aruiz@gmail.com,arquitecta +Luis,Martínez,29,lmartinez@gmail.com,profesor +Laura,Domínguez,41,ldominguez@gmail.com,enfermera +Pedro,Fernández,33,pfernandez@gmail.com,diseñador +Lucía,Ramos,26,lramos@gmail.com,psicóloga +Diego,Navarro,39,dnavarro@gmail.com,empresario +Paula,Hernández,31,phernandez@gmail.com,publicista \ No newline at end of file From 481a71e2ff660be4aa0229c9511d036f7102f1e8 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 8 Sep 2024 01:48:08 -0300 Subject: [PATCH 045/103] added Expression evaluator --- src/query/executor/mod.rs | 9 +++-- src/query/executor/select.rs | 15 +++++++- src/query/structs/comparator.rs | 54 ++++++++++++++++++++++++++ src/query/structs/expression.rs | 67 ++++++++++++++++++++++++++++++++- src/query/structs/mod.rs | 1 + src/utils/errors.rs | 9 ++++- src/utils/files.rs | 7 ++++ 7 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 src/query/structs/comparator.rs diff --git a/src/query/executor/mod.rs b/src/query/executor/mod.rs index f7febe2..8e83c11 100644 --- a/src/query/executor/mod.rs +++ b/src/query/executor/mod.rs @@ -2,8 +2,9 @@ use crate::errored; use crate::query::structs::operation::Operation::*; use crate::query::structs::query::Query; use crate::utils::errors::Errored; -use crate::utils::errors::Errored::Syntax; -use crate::utils::files::get_table_file; +use crate::utils::errors::Errored::{Syntax, Table}; +use crate::utils::files::{get_table_file, read_csv_line}; +use std::collections::HashMap; mod delete; mod insert; @@ -13,6 +14,7 @@ mod update; pub struct Executor { path: String, query: Query, + values: HashMap, } impl Executor { @@ -20,11 +22,12 @@ impl Executor { Executor { path: path.to_string(), query, + values: HashMap::new(), } } pub fn run(path: &str, query: Query) -> Result<(), Errored> { - let executor = Executor::new(path, query); + let mut executor = Executor::new(path, query); let table = get_table_file(&executor.path, &executor.query.table)?; match executor.query.operation { Select => executor.run_select(table), diff --git a/src/query/executor/select.rs b/src/query/executor/select.rs index 4f74a0d..c37020f 100644 --- a/src/query/executor/select.rs +++ b/src/query/executor/select.rs @@ -1,9 +1,22 @@ use crate::query::executor::Executor; use crate::utils::errors::Errored; +use crate::utils::files::read_csv_line; use std::fs::File; +use std::io::{BufRead, BufReader}; impl Executor { - pub fn run_select(&self, table: File) -> Result<(), Errored> { + pub fn run_select(&mut self, table: File) -> Result<(), Errored> { + let mut reader = BufReader::new(table); + + let mut header = String::new(); + reader.read_line(&mut header)?; + let header_fields = read_csv_line(&header); + println!("{}", header_fields.join(", ")); + for line in reader.lines() { + let l = line?; + println!("{:?}", read_csv_line(&l)); + } + Ok(()) } } diff --git a/src/query/structs/comparator.rs b/src/query/structs/comparator.rs new file mode 100644 index 0000000..291b1a0 --- /dev/null +++ b/src/query/structs/comparator.rs @@ -0,0 +1,54 @@ +use crate::errored; +use crate::query::structs::expression::ExpressionResult::Bool; +use crate::query::structs::expression::{ExpressionOperator, ExpressionResult}; +use crate::utils::errors::Errored; +use crate::utils::errors::Errored::Syntax; + +pub struct ExpressionComparator; + +impl ExpressionComparator { + pub fn compare_ints( + l: i64, + r: i64, + op: &ExpressionOperator, + ) -> Result { + match op { + ExpressionOperator::Equals => Ok(Bool(l == r)), + ExpressionOperator::NotEquals => Ok(Bool(l != r)), + ExpressionOperator::GreaterThan => Ok(Bool(l > r)), + ExpressionOperator::LessThan => Ok(Bool(l < r)), + ExpressionOperator::GreaterOrEqual => Ok(Bool(l >= r)), + ExpressionOperator::LessOrEqual => Ok(Bool(l <= r)), + _ => errored!(Syntax, "invalid comparison for ints: {:?}", op), + } + } + + pub fn compare_str( + l: &str, + r: &str, + op: &ExpressionOperator, + ) -> Result { + match op { + ExpressionOperator::Equals => Ok(Bool(l == r)), + ExpressionOperator::NotEquals => Ok(Bool(l != r)), + ExpressionOperator::GreaterThan => Ok(Bool(l > r)), + ExpressionOperator::LessThan => Ok(Bool(l < r)), + ExpressionOperator::GreaterOrEqual => Ok(Bool(l >= r)), + ExpressionOperator::LessOrEqual => Ok(Bool(l <= r)), + _ => errored!(Syntax, "invalid comparison for str: {:?}", op), + } + } + + pub fn compare_bools( + l: bool, + r: bool, + op: &ExpressionOperator, + ) -> Result { + match op { + ExpressionOperator::And => Ok(Bool(l && r)), + ExpressionOperator::Or => Ok(Bool(l || r)), + ExpressionOperator::Not => Ok(Bool(!l)), + _ => errored!(Syntax, "invalid comparison for bool: {:?}", op), + } + } +} diff --git a/src/query/structs/expression.rs b/src/query/structs/expression.rs index aab80ba..11602cd 100644 --- a/src/query/structs/expression.rs +++ b/src/query/structs/expression.rs @@ -1,4 +1,10 @@ -use crate::query::structs::token::Token; +use crate::errored; +use crate::query::structs::comparator::ExpressionComparator; +use crate::query::structs::expression::ExpressionResult::{Bool, Int, Str}; +use crate::query::structs::token::{Token, TokenKind}; +use crate::utils::errors::Errored; +use crate::utils::errors::Errored::Syntax; +use std::collections::HashMap; use std::fmt::{Debug, Formatter}; #[derive(Default, PartialEq)] @@ -28,6 +34,65 @@ pub enum ExpressionOperator { Not, } +pub enum ExpressionResult { + Int(i64), + Str(String), + Bool(bool), +} + +impl ExpressionNode { + pub fn evaluate(&self, values: &HashMap) -> Result { + match self { + ExpressionNode::Empty => Ok(Bool(false)), + ExpressionNode::Leaf(t) => match t.kind { + TokenKind::Identifier => ExpressionNode::get_variable_value(values, t), + TokenKind::String => Ok(Str(t.value.to_string())), + TokenKind::Number => Ok(Int(t.value.parse::()?)), + _ => Ok(Bool(false)), + }, + ExpressionNode::Statement { + operator, + left, + right, + } => { + let l = left.evaluate(values)?; + let r = right.evaluate(values)?; + ExpressionNode::get_statement_value(operator, l, r) + } + } + } + + fn get_statement_value( + operator: &ExpressionOperator, + left: ExpressionResult, + right: ExpressionResult, + ) -> Result { + match (left, right) { + (Int(l), Int(r)) => ExpressionComparator::compare_ints(l, r, operator), + (Str(l), Str(r)) => ExpressionComparator::compare_str(&l, &r, operator), + (Bool(l), Bool(r)) => ExpressionComparator::compare_bools(l, r, operator), + _ => errored!(Syntax, ""), + } + } + + fn get_variable_value( + values: &HashMap, + t: &Token, + ) -> Result { + let value = values.get(&t.value); + match value { + Some(v) => { + if v.parse::().is_ok() { + Ok(Int(v.parse::()?)) + } else { + Ok(Str(v.to_string())) + } + } + None => Ok(Bool(false)), + } + } +} + impl Debug for ExpressionNode { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { diff --git a/src/query/structs/mod.rs b/src/query/structs/mod.rs index c74eca9..b862857 100644 --- a/src/query/structs/mod.rs +++ b/src/query/structs/mod.rs @@ -1,3 +1,4 @@ +pub mod comparator; pub mod expression; pub mod operation; pub mod ordering; diff --git a/src/utils/errors.rs b/src/utils/errors.rs index 48ad1ca..efab932 100644 --- a/src/utils/errors.rs +++ b/src/utils/errors.rs @@ -2,6 +2,7 @@ use crate::utils::errors::Errored::*; use std::error::Error; use std::fmt::{Debug, Display, Formatter}; use std::io; +use std::num::ParseIntError; #[macro_export] macro_rules! errored { @@ -50,6 +51,12 @@ impl Display for Errored { impl From for Errored { fn from(value: io::Error) -> Self { - Default(format!("IO Error: {}", value)) + Default(format!("[IO]: {}", value)) + } +} + +impl From for Errored { + fn from(value: ParseIntError) -> Self { + Default(format!("[PARSE_INT]: {}", value)) } } diff --git a/src/utils/files.rs b/src/utils/files.rs index 3dbc3cb..3baaa5f 100644 --- a/src/utils/files.rs +++ b/src/utils/files.rs @@ -5,6 +5,13 @@ use std::fs::File; use std::path::Path; const CSV_EXTENSION: &str = ".csv"; +const CSV_SEPARATOR: &str = ","; + +pub fn read_csv_line(line: &str) -> Vec<&str> { + line.split(CSV_SEPARATOR) + .map(|s| s.trim()) + .collect::>() +} pub fn validate_path(dir: &str) -> Result<&Path, Errored> { let path = Path::new(dir); From 935b229933ffedde746d3ea7a0e4bdee2dab6eee Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 8 Sep 2024 01:49:22 -0300 Subject: [PATCH 046/103] forgot error message --- src/query/structs/expression.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/query/structs/expression.rs b/src/query/structs/expression.rs index 11602cd..c022ff8 100644 --- a/src/query/structs/expression.rs +++ b/src/query/structs/expression.rs @@ -71,7 +71,7 @@ impl ExpressionNode { (Int(l), Int(r)) => ExpressionComparator::compare_ints(l, r, operator), (Str(l), Str(r)) => ExpressionComparator::compare_str(&l, &r, operator), (Bool(l), Bool(r)) => ExpressionComparator::compare_bools(l, r, operator), - _ => errored!(Syntax, ""), + _ => errored!(Syntax, "expression members must match in type."), } } From ad93b012b9d3cef21530f7cd4d880ecd0532acf6 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 8 Sep 2024 01:52:45 -0300 Subject: [PATCH 047/103] forgot another error message --- src/query/structs/expression.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/query/structs/expression.rs b/src/query/structs/expression.rs index c022ff8..41906c8 100644 --- a/src/query/structs/expression.rs +++ b/src/query/structs/expression.rs @@ -3,7 +3,7 @@ use crate::query::structs::comparator::ExpressionComparator; use crate::query::structs::expression::ExpressionResult::{Bool, Int, Str}; use crate::query::structs::token::{Token, TokenKind}; use crate::utils::errors::Errored; -use crate::utils::errors::Errored::Syntax; +use crate::utils::errors::Errored::{Column, Syntax}; use std::collections::HashMap; use std::fmt::{Debug, Formatter}; @@ -88,7 +88,7 @@ impl ExpressionNode { Ok(Str(v.to_string())) } } - None => Ok(Bool(false)), + None => errored!(Column, "column {} does not exist", t.value), } } } From dec953529799217f6e8ad029e1229c5ed6e5f768 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 8 Sep 2024 03:40:48 -0300 Subject: [PATCH 048/103] improved select execution --- src/query/executor/mod.rs | 4 +- src/query/executor/select.rs | 20 ++++++---- src/query/structs/expression.rs | 1 + src/query/structs/mod.rs | 1 + src/query/structs/row.rs | 66 +++++++++++++++++++++++++++++++++ src/utils/files.rs | 13 +++++-- tests/clientes.csv | 7 ++++ tests/person.csv | 11 ------ 8 files changed, 99 insertions(+), 24 deletions(-) create mode 100644 src/query/structs/row.rs create mode 100644 tests/clientes.csv delete mode 100644 tests/person.csv diff --git a/src/query/executor/mod.rs b/src/query/executor/mod.rs index 8e83c11..85a4cbc 100644 --- a/src/query/executor/mod.rs +++ b/src/query/executor/mod.rs @@ -2,8 +2,8 @@ use crate::errored; use crate::query::structs::operation::Operation::*; use crate::query::structs::query::Query; use crate::utils::errors::Errored; -use crate::utils::errors::Errored::{Syntax, Table}; -use crate::utils::files::{get_table_file, read_csv_line}; +use crate::utils::errors::Errored::Syntax; +use crate::utils::files::get_table_file; use std::collections::HashMap; mod delete; diff --git a/src/query/executor/select.rs b/src/query/executor/select.rs index c37020f..bfe5298 100644 --- a/src/query/executor/select.rs +++ b/src/query/executor/select.rs @@ -1,22 +1,26 @@ use crate::query::executor::Executor; +use crate::query::structs::row::Row; use crate::utils::errors::Errored; -use crate::utils::files::read_csv_line; +use crate::utils::files::{extract_header, split_csv}; use std::fs::File; use std::io::{BufRead, BufReader}; impl Executor { pub fn run_select(&mut self, table: File) -> Result<(), Errored> { let mut reader = BufReader::new(table); - - let mut header = String::new(); - reader.read_line(&mut header)?; - let header_fields = read_csv_line(&header); - println!("{}", header_fields.join(", ")); + let header = extract_header(&mut reader)?; + println!("{}", header.join(",")); + let mut matched_rows: Vec = vec![]; for line in reader.lines() { let l = line?; - println!("{:?}", read_csv_line(&l)); + let fields = split_csv(&l); + let mut row = Row::new(&header); + row.set_new_values(fields)?; + if row.matches_condition(&self.query)? { + matched_rows.push(row) + } } - + //todo: implement ordering. Ok(()) } } diff --git a/src/query/structs/expression.rs b/src/query/structs/expression.rs index 41906c8..a5de3ee 100644 --- a/src/query/structs/expression.rs +++ b/src/query/structs/expression.rs @@ -34,6 +34,7 @@ pub enum ExpressionOperator { Not, } +#[derive(Debug, PartialEq)] pub enum ExpressionResult { Int(i64), Str(String), diff --git a/src/query/structs/mod.rs b/src/query/structs/mod.rs index b862857..8a5056d 100644 --- a/src/query/structs/mod.rs +++ b/src/query/structs/mod.rs @@ -3,4 +3,5 @@ pub mod expression; pub mod operation; pub mod ordering; pub mod query; +pub mod row; pub mod token; diff --git a/src/query/structs/row.rs b/src/query/structs/row.rs new file mode 100644 index 0000000..e80eb58 --- /dev/null +++ b/src/query/structs/row.rs @@ -0,0 +1,66 @@ +use crate::errored; +use crate::query::structs::expression::ExpressionResult; +use crate::query::structs::query::Query; +use crate::utils::errors::Errored; +use crate::utils::errors::Errored::{Column, Syntax, Table}; +use std::collections::HashMap; +use std::fmt::{Debug, Display}; + +pub struct Row<'a> { + pub header: &'a Vec, + pub values: HashMap, +} + +impl<'a> Row<'a> { + pub fn new(header: &'a Vec) -> Self { + Self { + header, + values: HashMap::new(), + } + } + + pub fn insert(&mut self, key: &str, value: String) -> Result<(), Errored> { + if self.header.contains(&key.to_string()) { + self.values.insert(key.to_string(), value); + } else { + errored!( + Column, + "column {} does not exist in table with fields: {:?}", + key, + self.header + ) + } + Ok(()) + } + + pub fn set_new_values(&mut self, values: Vec) -> Result<(), Errored> { + if self.header.len() != values.len() { + errored!( + Table, + "new row ({}) has less fields than table needs ({}).", + values.len(), + self.header.len() + ); + } + for (key, value) in self.header.iter().zip(values) { + self.values.insert(key.to_string(), value); + } + Ok(()) + } + + pub fn print_values(&self) { + let mut fields: Vec<&str> = Vec::new(); + for key in self.header { + let value = self.values.get(key).map(|v| v.as_str()).unwrap_or(""); + fields.push(value); + } + println!("{}", fields.join(",")); + } + + pub fn matches_condition(&self, query: &Query) -> Result { + match query.conditions.evaluate(&self.values)? { + ExpressionResult::Bool(b) => Ok(b), + _ => errored!(Syntax, "query condition evaluates to non-boolean value."), + } + } +} diff --git a/src/utils/files.rs b/src/utils/files.rs index 3baaa5f..789648d 100644 --- a/src/utils/files.rs +++ b/src/utils/files.rs @@ -2,15 +2,22 @@ use crate::errored; use crate::utils::errors::Errored; use crate::utils::errors::Errored::{Default, Table}; use std::fs::File; +use std::io::{BufRead, BufReader}; use std::path::Path; const CSV_EXTENSION: &str = ".csv"; const CSV_SEPARATOR: &str = ","; -pub fn read_csv_line(line: &str) -> Vec<&str> { +pub fn extract_header(reader: &mut BufReader) -> Result, Errored> { + let mut header = String::new(); + reader.read_line(&mut header)?; + Ok(split_csv(&header)) +} + +pub fn split_csv(line: &str) -> Vec { line.split(CSV_SEPARATOR) - .map(|s| s.trim()) - .collect::>() + .map(|s| s.trim().to_string()) + .collect::>() } pub fn validate_path(dir: &str) -> Result<&Path, Errored> { diff --git a/tests/clientes.csv b/tests/clientes.csv new file mode 100644 index 0000000..85310a6 --- /dev/null +++ b/tests/clientes.csv @@ -0,0 +1,7 @@ +id,nombre,apellido,email +1,Juan,Pérez,juan.perez@email.com +2,Ana,López,ana.lopez@email.com +3,Carlos,Gómez,carlos.gomez@email.com +4,María,Rodríguez,maria.rodriguez@email.com +5,José,López,jose.lopez@email.com +6,Laura,Fernández,laura.fernandez@email.com diff --git a/tests/person.csv b/tests/person.csv deleted file mode 100644 index 94a0d3c..0000000 --- a/tests/person.csv +++ /dev/null @@ -1,11 +0,0 @@ -Nombre,Apellido,Edad,Correo electronico,Profesion -Juan,Perez,32,jperez@gmail.com,medico -Maria,Gomez,28,mgomez@gmail.com,abogado -Carlos,Sánchez,45,csanchez@gmail.com,ingeniero -Ana,Ruiz,36,aruiz@gmail.com,arquitecta -Luis,Martínez,29,lmartinez@gmail.com,profesor -Laura,Domínguez,41,ldominguez@gmail.com,enfermera -Pedro,Fernández,33,pfernandez@gmail.com,diseñador -Lucía,Ramos,26,lramos@gmail.com,psicóloga -Diego,Navarro,39,dnavarro@gmail.com,empresario -Paula,Hernández,31,phernandez@gmail.com,publicista \ No newline at end of file From 1383f091b2b03217fc8b7f515a3a5f1775d95632 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 8 Sep 2024 03:41:37 -0300 Subject: [PATCH 049/103] removed imports --- src/query/executor/mod.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/query/executor/mod.rs b/src/query/executor/mod.rs index 85a4cbc..b17a690 100644 --- a/src/query/executor/mod.rs +++ b/src/query/executor/mod.rs @@ -4,7 +4,6 @@ use crate::query::structs::query::Query; use crate::utils::errors::Errored; use crate::utils::errors::Errored::Syntax; use crate::utils::files::get_table_file; -use std::collections::HashMap; mod delete; mod insert; @@ -14,7 +13,6 @@ mod update; pub struct Executor { path: String, query: Query, - values: HashMap, } impl Executor { @@ -22,7 +20,6 @@ impl Executor { Executor { path: path.to_string(), query, - values: HashMap::new(), } } From e5fa8da6f4bc7b13e03c88f09186809964f158e8 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 8 Sep 2024 11:15:05 -0300 Subject: [PATCH 050/103] finished select ordering and first implementation of select. --- src/query/executor/select.rs | 28 +++++++++++++++++++++++++++- src/query/structs/comparator.rs | 18 +++++++++++++++++- src/query/structs/expression.rs | 2 +- src/query/structs/row.rs | 5 ++--- 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/query/executor/select.rs b/src/query/executor/select.rs index bfe5298..5d0f7f5 100644 --- a/src/query/executor/select.rs +++ b/src/query/executor/select.rs @@ -1,7 +1,10 @@ use crate::query::executor::Executor; +use crate::query::structs::expression::ExpressionNode; +use crate::query::structs::ordering::OrderKind; use crate::query::structs::row::Row; use crate::utils::errors::Errored; use crate::utils::files::{extract_header, split_csv}; +use std::cmp::Ordering; use std::fs::File; use std::io::{BufRead, BufReader}; @@ -20,7 +23,30 @@ impl Executor { matched_rows.push(row) } } - //todo: implement ordering. + self.sort_rows(&mut matched_rows); + self.output_rows(&matched_rows); Ok(()) } + + fn sort_rows(&mut self, matched_rows: &mut [Row]) { + for order in &self.query.ordering { + matched_rows.sort_by(|a, b| { + let l = ExpressionNode::get_variable_value(&a.values, &order.field); + let r = ExpressionNode::get_variable_value(&b.values, &order.field); + if let (Ok(a), Ok(b)) = (l, r) { + return match order.kind { + OrderKind::Asc => a.compare(&b).unwrap_or(Ordering::Equal), + OrderKind::Desc => b.compare(&a).unwrap_or(Ordering::Equal), + }; + } + Ordering::Equal + }) + } + } + + fn output_rows(&self, matched_rows: &[Row]) { + for row in matched_rows { + row.print_values() + } + } } diff --git a/src/query/structs/comparator.rs b/src/query/structs/comparator.rs index 291b1a0..cde7f96 100644 --- a/src/query/structs/comparator.rs +++ b/src/query/structs/comparator.rs @@ -1,5 +1,5 @@ use crate::errored; -use crate::query::structs::expression::ExpressionResult::Bool; +use crate::query::structs::expression::ExpressionResult::{Bool, Int, Str}; use crate::query::structs::expression::{ExpressionOperator, ExpressionResult}; use crate::utils::errors::Errored; use crate::utils::errors::Errored::Syntax; @@ -52,3 +52,19 @@ impl ExpressionComparator { } } } + +impl ExpressionResult { + pub fn compare(&self, other: &ExpressionResult) -> Result { + match (self, other) { + (Int(a), Int(b)) => Ok(a.cmp(b)), + (Str(a), Str(b)) => Ok(a.cmp(b)), + (Bool(a), Bool(b)) => Ok(a.cmp(b)), + _ => errored!( + Syntax, + "Cannot compare different types: {:?} and {:?}", + self, + other + ), + } + } +} diff --git a/src/query/structs/expression.rs b/src/query/structs/expression.rs index a5de3ee..e31c41e 100644 --- a/src/query/structs/expression.rs +++ b/src/query/structs/expression.rs @@ -76,7 +76,7 @@ impl ExpressionNode { } } - fn get_variable_value( + pub fn get_variable_value( values: &HashMap, t: &Token, ) -> Result { diff --git a/src/query/structs/row.rs b/src/query/structs/row.rs index e80eb58..04398b4 100644 --- a/src/query/structs/row.rs +++ b/src/query/structs/row.rs @@ -4,7 +4,6 @@ use crate::query::structs::query::Query; use crate::utils::errors::Errored; use crate::utils::errors::Errored::{Column, Syntax, Table}; use std::collections::HashMap; -use std::fmt::{Debug, Display}; pub struct Row<'a> { pub header: &'a Vec, @@ -19,7 +18,7 @@ impl<'a> Row<'a> { } } - pub fn insert(&mut self, key: &str, value: String) -> Result<(), Errored> { + fn insert(&mut self, key: &str, value: String) -> Result<(), Errored> { if self.header.contains(&key.to_string()) { self.values.insert(key.to_string(), value); } else { @@ -43,7 +42,7 @@ impl<'a> Row<'a> { ); } for (key, value) in self.header.iter().zip(values) { - self.values.insert(key.to_string(), value); + self.insert(key, value)?; } Ok(()) } From 6b61bccefd4b89644c59f050ea8606574977a477 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 8 Sep 2024 11:23:45 -0300 Subject: [PATCH 051/103] slight refactors to reduce clippy alerts --- src/query/builder/mod.rs | 12 +++++------- src/query/executor/select.rs | 9 +++++++-- src/query/structs/comparator.rs | 11 ++++++----- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/query/builder/mod.rs b/src/query/builder/mod.rs index 03bff65..a6b38e6 100644 --- a/src/query/builder/mod.rs +++ b/src/query/builder/mod.rs @@ -23,14 +23,12 @@ pub trait Builder { fn tokens(&mut self) -> &mut VecDeque; fn parse_table(&mut self, operation: Operation) -> Result { - if let Some(t) = self.tokens().front() { - match operation { - Select | Delete => { - self.peek_expecting("FROM", Keyword)?; - self.tokens().pop_front(); - } - _ => {} + match operation { + Select | Delete => { + self.peek_expecting("FROM", Keyword)?; + self.tokens().pop_front(); } + _ => {} } let t = self .tokens() diff --git a/src/query/executor/select.rs b/src/query/executor/select.rs index 5d0f7f5..26e0c35 100644 --- a/src/query/executor/select.rs +++ b/src/query/executor/select.rs @@ -1,4 +1,5 @@ use crate::query::executor::Executor; +use crate::query::structs::comparator::ExpressionComparator; use crate::query::structs::expression::ExpressionNode; use crate::query::structs::ordering::OrderKind; use crate::query::structs::row::Row; @@ -35,8 +36,12 @@ impl Executor { let r = ExpressionNode::get_variable_value(&b.values, &order.field); if let (Ok(a), Ok(b)) = (l, r) { return match order.kind { - OrderKind::Asc => a.compare(&b).unwrap_or(Ordering::Equal), - OrderKind::Desc => b.compare(&a).unwrap_or(Ordering::Equal), + OrderKind::Asc => { + ExpressionComparator::cmp(&a, &b).unwrap_or(Ordering::Equal) + } + OrderKind::Desc => { + ExpressionComparator::cmp(&b, &a).unwrap_or(Ordering::Equal) + } }; } Ordering::Equal diff --git a/src/query/structs/comparator.rs b/src/query/structs/comparator.rs index cde7f96..3bc8aee 100644 --- a/src/query/structs/comparator.rs +++ b/src/query/structs/comparator.rs @@ -51,18 +51,19 @@ impl ExpressionComparator { _ => errored!(Syntax, "invalid comparison for bool: {:?}", op), } } -} -impl ExpressionResult { - pub fn compare(&self, other: &ExpressionResult) -> Result { - match (self, other) { + pub fn cmp( + this: &ExpressionResult, + other: &ExpressionResult, + ) -> Result { + match (this, other) { (Int(a), Int(b)) => Ok(a.cmp(b)), (Str(a), Str(b)) => Ok(a.cmp(b)), (Bool(a), Bool(b)) => Ok(a.cmp(b)), _ => errored!( Syntax, "Cannot compare different types: {:?} and {:?}", - self, + this, other ), } From 17b2b8c208b0e4f10fffe7ef670cea8d9922f4b1 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 8 Sep 2024 11:32:28 -0300 Subject: [PATCH 052/103] refactor and new row methods --- src/main.rs | 3 +-- src/query/executor/select.rs | 4 ++-- src/query/structs/comparator.rs | 2 +- src/query/structs/row.rs | 20 ++++++++++++++++++-- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index e85ace2..dfbce2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,12 +9,11 @@ use std::error::Error; mod query; mod utils; -fn main() -> Result<(), Box> { +fn main() { let args = env::args().collect(); if let Err(e) = run(args) { println!("{}", e); } - Ok(()) } fn run(args: Vec) -> Result<(), Box> { diff --git a/src/query/executor/select.rs b/src/query/executor/select.rs index 26e0c35..838d389 100644 --- a/src/query/executor/select.rs +++ b/src/query/executor/select.rs @@ -37,10 +37,10 @@ impl Executor { if let (Ok(a), Ok(b)) = (l, r) { return match order.kind { OrderKind::Asc => { - ExpressionComparator::cmp(&a, &b).unwrap_or(Ordering::Equal) + ExpressionComparator::compare_ordering(&a, &b).unwrap_or(Ordering::Equal) } OrderKind::Desc => { - ExpressionComparator::cmp(&b, &a).unwrap_or(Ordering::Equal) + ExpressionComparator::compare_ordering(&b, &a).unwrap_or(Ordering::Equal) } }; } diff --git a/src/query/structs/comparator.rs b/src/query/structs/comparator.rs index 3bc8aee..e759cfc 100644 --- a/src/query/structs/comparator.rs +++ b/src/query/structs/comparator.rs @@ -52,7 +52,7 @@ impl ExpressionComparator { } } - pub fn cmp( + pub fn compare_ordering( this: &ExpressionResult, other: &ExpressionResult, ) -> Result { diff --git a/src/query/structs/row.rs b/src/query/structs/row.rs index 04398b4..7cd6eda 100644 --- a/src/query/structs/row.rs +++ b/src/query/structs/row.rs @@ -31,6 +31,18 @@ impl<'a> Row<'a> { } Ok(()) } + + pub fn clear(&mut self) -> Result<(), Errored> { + for (key, _) in self.header.iter().zip(values) { + self.insert(key, "".to_string())?; + } + Ok(()) + } + + pub fn update_value(&mut self, key: String, value: String) -> Result<(), Errored> { + self.insert(&key, value)?; + Ok(()) + } pub fn set_new_values(&mut self, values: Vec) -> Result<(), Errored> { if self.header.len() != values.len() { @@ -47,13 +59,17 @@ impl<'a> Row<'a> { Ok(()) } - pub fn print_values(&self) { + pub fn as_csv_string(&self) -> String { let mut fields: Vec<&str> = Vec::new(); for key in self.header { let value = self.values.get(key).map(|v| v.as_str()).unwrap_or(""); fields.push(value); } - println!("{}", fields.join(",")); + fields.join(",") + } + + pub fn print_values(&self) { + println!("{}", self.as_csv_string()); } pub fn matches_condition(&self, query: &Query) -> Result { From 7d1981b5a56cbf69666e3bc92041e5106f972dac Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 8 Sep 2024 14:17:16 -0300 Subject: [PATCH 053/103] allowed for multiple inserts in one query. --- src/query/builder/insert.rs | 26 ++++++++++++++++++++++++-- src/query/builder/mod.rs | 6 +++++- src/query/executor/select.rs | 12 +++++------- src/query/executor/update.rs | 7 ++++++- src/query/structs/query.rs | 10 +++++++--- src/query/structs/row.rs | 4 ++-- src/utils/files.rs | 2 +- 7 files changed, 50 insertions(+), 17 deletions(-) diff --git a/src/query/builder/insert.rs b/src/query/builder/insert.rs index 66ffd77..0d9aa47 100644 --- a/src/query/builder/insert.rs +++ b/src/query/builder/insert.rs @@ -1,9 +1,11 @@ +use crate::errored; use crate::query::builder::{unexpected_token_in_stage, validate_keywords, Builder}; use crate::query::structs::operation::Operation::Insert; use crate::query::structs::query::Query; use crate::query::structs::token::TokenKind::{Keyword, ParenthesisClose, ParenthesisOpen}; use crate::query::structs::token::{Token, TokenKind}; use crate::utils::errors::Errored; +use crate::utils::errors::Errored::Syntax; use std::collections::VecDeque; const ALLOWED_KEYWORDS: &[&str] = &["VALUES"]; @@ -17,9 +19,10 @@ impl InsertBuilder { Self { tokens } } - fn parse_insert_values(&mut self) -> Result, Errored> { + fn parse_insert_values(&mut self) -> Result>, Errored> { self.pop_expecting("VALUES", Keyword)?; self.peek_expecting("(", ParenthesisOpen)?; + let mut inserts = vec![]; let mut values = vec![]; while let Some(t) = self.tokens.front() { match t.kind { @@ -33,11 +36,29 @@ impl InsertBuilder { } ParenthesisClose => { self.tokens.pop_front(); + inserts.push(values); + values = vec![]; } _ => unexpected_token_in_stage("VALUES", t)?, } } - Ok(values) + Ok(inserts) + } + + fn validate_inserts(&self, query: &Query) -> Result<(), Errored> { + for insert in &query.inserts { + let columns = query.columns.len(); + if insert.len() != columns { + let values: Vec<&String> = insert.iter().map(|t| &t.value).collect(); + errored!( + Syntax, + "expected {} columns but insert has:\n{:?}", + columns, + values + ) + } + } + Ok(()) } } @@ -51,6 +72,7 @@ impl Builder for InsertBuilder { query.columns = self.parse_columns()?; query.inserts = self.parse_insert_values()?; self.expect_none()?; + self.validate_inserts(&query)?; Ok(query) } diff --git a/src/query/builder/mod.rs b/src/query/builder/mod.rs index a6b38e6..cbc48e8 100644 --- a/src/query/builder/mod.rs +++ b/src/query/builder/mod.rs @@ -52,7 +52,11 @@ pub trait Builder { Keyword if t.value == "FROM" || t.value == "VALUES" => { break; } - ParenthesisClose | Operator if t.value == "*" => { + ParenthesisClose => { + self.tokens().pop_front(); + break; + } + Operator if t.value == "*" => { self.tokens().pop_front(); break; } diff --git a/src/query/executor/select.rs b/src/query/executor/select.rs index 838d389..92832e2 100644 --- a/src/query/executor/select.rs +++ b/src/query/executor/select.rs @@ -11,7 +11,7 @@ use std::io::{BufRead, BufReader}; impl Executor { pub fn run_select(&mut self, table: File) -> Result<(), Errored> { - let mut reader = BufReader::new(table); + let mut reader = BufReader::new(&table); let header = extract_header(&mut reader)?; println!("{}", header.join(",")); let mut matched_rows: Vec = vec![]; @@ -36,12 +36,10 @@ impl Executor { let r = ExpressionNode::get_variable_value(&b.values, &order.field); if let (Ok(a), Ok(b)) = (l, r) { return match order.kind { - OrderKind::Asc => { - ExpressionComparator::compare_ordering(&a, &b).unwrap_or(Ordering::Equal) - } - OrderKind::Desc => { - ExpressionComparator::compare_ordering(&b, &a).unwrap_or(Ordering::Equal) - } + OrderKind::Asc => ExpressionComparator::compare_ordering(&a, &b) + .unwrap_or(Ordering::Equal), + OrderKind::Desc => ExpressionComparator::compare_ordering(&b, &a) + .unwrap_or(Ordering::Equal), }; } Ordering::Equal diff --git a/src/query/executor/update.rs b/src/query/executor/update.rs index 37b1d4e..8413d61 100644 --- a/src/query/executor/update.rs +++ b/src/query/executor/update.rs @@ -1,9 +1,14 @@ use crate::query::executor::Executor; use crate::utils::errors::Errored; +use crate::utils::files::extract_header; use std::fs::File; +use std::io::{BufReader, BufWriter}; impl Executor { pub fn run_update(&self, table: File) -> Result<(), Errored> { - todo!() + let mut writer = BufWriter::new(&table); + let mut reader = BufReader::new(&table); + let header = extract_header(&mut reader)?; + Ok(()) } } diff --git a/src/query/structs/query.rs b/src/query/structs/query.rs index 8cd9a2c..48a53df 100644 --- a/src/query/structs/query.rs +++ b/src/query/structs/query.rs @@ -18,7 +18,7 @@ pub struct Query { pub operation: Operation, pub table: String, pub columns: Vec, - pub inserts: Vec, + pub inserts: Vec>, pub updates: Vec, pub conditions: ExpressionNode, pub ordering: Vec, @@ -53,11 +53,15 @@ impl Query { impl Display for Query { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let fields: Vec<&str> = self.columns.iter().map(|f| f.value.as_str()).collect(); - let inserts: Vec<&str> = self.inserts.iter().map(|f| f.value.as_str()).collect(); writeln!(f, "Query Kind: [{:?}]", self.operation)?; writeln!(f, "Table: {:?}", self.table)?; writeln!(f, "Columns: {:?}", fields)?; - writeln!(f, "Inserts: {:?}", inserts)?; + writeln!(f, "Inserts {{ ")?; + for insert in &self.inserts { + let values: Vec<&String> = insert.iter().map(|t| &t.value).collect(); + writeln!(f, " {:?}", values)?; + } + writeln!(f, "}} ")?; writeln!(f, "Updates: {:?}", self.updates)?; writeln!(f, "Conditions: {:?}", self.conditions)?; writeln!(f, "Ordering: {:?}", self.ordering) diff --git a/src/query/structs/row.rs b/src/query/structs/row.rs index 7cd6eda..c8c3574 100644 --- a/src/query/structs/row.rs +++ b/src/query/structs/row.rs @@ -31,9 +31,9 @@ impl<'a> Row<'a> { } Ok(()) } - + pub fn clear(&mut self) -> Result<(), Errored> { - for (key, _) in self.header.iter().zip(values) { + for key in self.header.iter() { self.insert(key, "".to_string())?; } Ok(()) diff --git a/src/utils/files.rs b/src/utils/files.rs index 789648d..9a35d09 100644 --- a/src/utils/files.rs +++ b/src/utils/files.rs @@ -8,7 +8,7 @@ use std::path::Path; const CSV_EXTENSION: &str = ".csv"; const CSV_SEPARATOR: &str = ","; -pub fn extract_header(reader: &mut BufReader) -> Result, Errored> { +pub fn extract_header(reader: &mut BufReader<&File>) -> Result, Errored> { let mut header = String::new(); reader.read_line(&mut header)?; Ok(split_csv(&header)) From 8c82097e64d932efd5a62154782113e34a572521 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 8 Sep 2024 14:52:44 -0300 Subject: [PATCH 054/103] improved file handling in executor --- src/query/executor/delete.rs | 3 +-- src/query/executor/insert.rs | 3 +-- src/query/executor/mod.rs | 24 +++++++++++------------- src/query/executor/select.rs | 6 +++--- src/query/executor/update.rs | 10 +++++----- src/utils/files.rs | 31 ++++++++++++++++--------------- 6 files changed, 37 insertions(+), 40 deletions(-) diff --git a/src/query/executor/delete.rs b/src/query/executor/delete.rs index 26b2b21..d48bed5 100644 --- a/src/query/executor/delete.rs +++ b/src/query/executor/delete.rs @@ -1,9 +1,8 @@ use crate::query::executor::Executor; use crate::utils::errors::Errored; -use std::fs::File; impl Executor { - pub fn run_delete(&self, table: File) -> Result<(), Errored> { + pub fn run_delete(&self) -> Result<(), Errored> { todo!() } } diff --git a/src/query/executor/insert.rs b/src/query/executor/insert.rs index d2ec977..72cc58b 100644 --- a/src/query/executor/insert.rs +++ b/src/query/executor/insert.rs @@ -1,9 +1,8 @@ use crate::query::executor::Executor; use crate::utils::errors::Errored; -use std::fs::File; impl Executor { - pub fn run_insert(&self, table: File) -> Result<(), Errored> { + pub fn run_insert(&self) -> Result<(), Errored> { todo!() } } diff --git a/src/query/executor/mod.rs b/src/query/executor/mod.rs index b17a690..a5e6a69 100644 --- a/src/query/executor/mod.rs +++ b/src/query/executor/mod.rs @@ -3,7 +3,8 @@ use crate::query::structs::operation::Operation::*; use crate::query::structs::query::Query; use crate::utils::errors::Errored; use crate::utils::errors::Errored::Syntax; -use crate::utils::files::get_table_file; +use crate::utils::files::get_table_path; +use std::path::{Path, PathBuf}; mod delete; mod insert; @@ -11,26 +12,23 @@ mod select; mod update; pub struct Executor { - path: String, + table_path: PathBuf, query: Query, } impl Executor { - pub fn new(path: &str, query: Query) -> Self { - Executor { - path: path.to_string(), - query, - } + fn new(table_path: PathBuf, query: Query) -> Self { + Executor { table_path, query } } pub fn run(path: &str, query: Query) -> Result<(), Errored> { - let mut executor = Executor::new(path, query); - let table = get_table_file(&executor.path, &executor.query.table)?; + let table_path = get_table_path(Path::new(path), &query.table)?; + let mut executor = Executor::new(table_path, query); match executor.query.operation { - Select => executor.run_select(table), - Update => executor.run_update(table), - Delete => executor.run_delete(table), - Insert => executor.run_insert(table), + Select => executor.run_select(), + Update => executor.run_update(), + Delete => executor.run_delete(), + Insert => executor.run_insert(), _ => errored!(Syntax, "unknown operation trying to be executed."), } } diff --git a/src/query/executor/select.rs b/src/query/executor/select.rs index 92832e2..d9aaf20 100644 --- a/src/query/executor/select.rs +++ b/src/query/executor/select.rs @@ -4,13 +4,13 @@ use crate::query::structs::expression::ExpressionNode; use crate::query::structs::ordering::OrderKind; use crate::query::structs::row::Row; use crate::utils::errors::Errored; -use crate::utils::files::{extract_header, split_csv}; +use crate::utils::files::{extract_header, get_table_file, split_csv}; use std::cmp::Ordering; -use std::fs::File; use std::io::{BufRead, BufReader}; impl Executor { - pub fn run_select(&mut self, table: File) -> Result<(), Errored> { + pub fn run_select(&mut self) -> Result<(), Errored> { + let table = get_table_file(&self.table_path)?; let mut reader = BufReader::new(&table); let header = extract_header(&mut reader)?; println!("{}", header.join(",")); diff --git a/src/query/executor/update.rs b/src/query/executor/update.rs index 8413d61..4de3b75 100644 --- a/src/query/executor/update.rs +++ b/src/query/executor/update.rs @@ -1,12 +1,12 @@ use crate::query::executor::Executor; use crate::utils::errors::Errored; -use crate::utils::files::extract_header; -use std::fs::File; -use std::io::{BufReader, BufWriter}; +use crate::utils::files::{extract_header, get_table_file, get_temp_file}; +use std::io::BufReader; impl Executor { - pub fn run_update(&self, table: File) -> Result<(), Errored> { - let mut writer = BufWriter::new(&table); + pub fn run_update(&self) -> Result<(), Errored> { + let table = get_table_file(&self.table_path)?; + let temp_table = get_temp_file(&self.table_path)?; let mut reader = BufReader::new(&table); let header = extract_header(&mut reader)?; Ok(()) diff --git a/src/utils/files.rs b/src/utils/files.rs index 9a35d09..c97c0a4 100644 --- a/src/utils/files.rs +++ b/src/utils/files.rs @@ -1,10 +1,11 @@ use crate::errored; use crate::utils::errors::Errored; -use crate::utils::errors::Errored::{Default, Table}; +use crate::utils::errors::Errored::Default; use std::fs::File; use std::io::{BufRead, BufReader}; -use std::path::Path; +use std::path::{Path, PathBuf}; +const TEMP_EXTENSION: &str = ".tmp"; const CSV_EXTENSION: &str = ".csv"; const CSV_SEPARATOR: &str = ","; @@ -32,24 +33,24 @@ pub fn validate_path(dir: &str) -> Result<&Path, Errored> { Ok(path) } -pub fn get_table_file(dir_path: &str, table_name: &str) -> Result { - let path = Path::new(dir_path); - let table_path = path.join(format!("{}{}", table_name, CSV_EXTENSION)); +pub fn get_table_path(dir_path: &Path, table_name: &str) -> Result { + let table_path = dir_path.join(format!("{}{}", table_name, CSV_EXTENSION)); if !table_path.is_file() { errored!( Default, "table {} does not exist in directory: {}", table_name, - dir_path + dir_path.display() ); } - match File::open(table_path) { - Ok(f) => Ok(f), - Err(err) => errored!( - Table, - "could not read table {} file, cause: {}", - table_name, - err - ), - } + Ok(table_path) +} + +pub fn get_temp_file(table_path: &Path) -> Result { + let table_path = table_path.with_extension(TEMP_EXTENSION); + Ok(File::create(table_path)?) +} + +pub fn get_table_file(table_path: &Path) -> Result { + Ok(File::open(table_path)?) } From 202873e10aa0bdc3fd3a4eca3902bffb0816a57f Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 8 Sep 2024 17:00:55 -0300 Subject: [PATCH 055/103] first implementation of update --- src/query/executor/delete.rs | 3 +- src/query/executor/insert.rs | 3 +- src/query/executor/update.rs | 25 ++++++++++++-- src/query/structs/expression.rs | 12 ++++++- src/query/structs/query.rs | 7 ++++ src/query/structs/row.rs | 16 ++++++--- src/utils/errors.rs | 4 +-- src/utils/files.rs | 61 +++++++++++++++++++++++---------- tests/{ => tables}/clientes.csv | 0 9 files changed, 101 insertions(+), 30 deletions(-) rename tests/{ => tables}/clientes.csv (100%) diff --git a/src/query/executor/delete.rs b/src/query/executor/delete.rs index d48bed5..05b0c43 100644 --- a/src/query/executor/delete.rs +++ b/src/query/executor/delete.rs @@ -3,6 +3,7 @@ use crate::utils::errors::Errored; impl Executor { pub fn run_delete(&self) -> Result<(), Errored> { - todo!() + //delete_temp_file(&self.table_path)?; + Ok(()) } } diff --git a/src/query/executor/insert.rs b/src/query/executor/insert.rs index 72cc58b..b4448bf 100644 --- a/src/query/executor/insert.rs +++ b/src/query/executor/insert.rs @@ -3,6 +3,7 @@ use crate::utils::errors::Errored; impl Executor { pub fn run_insert(&self) -> Result<(), Errored> { - todo!() + //delete_temp_file(&self.table_path)?; + Ok(()) } } diff --git a/src/query/executor/update.rs b/src/query/executor/update.rs index 4de3b75..c0bc9d1 100644 --- a/src/query/executor/update.rs +++ b/src/query/executor/update.rs @@ -1,14 +1,33 @@ use crate::query::executor::Executor; +use crate::query::structs::expression::ExpressionNode::Empty; +use crate::query::structs::row::Row; use crate::utils::errors::Errored; -use crate::utils::files::{extract_header, get_table_file, get_temp_file}; -use std::io::BufReader; +use crate::utils::files::{ + delete_temp_file, extract_header, get_table_file, get_temp_file, split_csv, +}; +use std::io::{BufRead, BufReader, BufWriter, Write}; impl Executor { pub fn run_update(&self) -> Result<(), Errored> { let table = get_table_file(&self.table_path)?; - let temp_table = get_temp_file(&self.table_path)?; + let (temp_table, temp_path) = get_temp_file(&self.query.table, &self.table_path)?; let mut reader = BufReader::new(&table); + let mut writer = BufWriter::new(temp_table); let header = extract_header(&mut reader)?; + writeln!(writer, "{}", header.join(","))?; + for line in reader.lines() { + let l = line?; + let fields = split_csv(&l); + let mut row = Row::new(&header); + row.set_new_values(fields)?; + if self.query.conditions == Empty || row.matches_condition(&self.query)? { + row.update_values(&self.query.updates)?; + writeln!(writer, "{}", row.as_csv_string())? + } else { + writeln!(writer, "{}", l)? + } + } + delete_temp_file(&self.table_path, &temp_path)?; Ok(()) } } diff --git a/src/query/structs/expression.rs b/src/query/structs/expression.rs index e31c41e..c096408 100644 --- a/src/query/structs/expression.rs +++ b/src/query/structs/expression.rs @@ -3,7 +3,7 @@ use crate::query::structs::comparator::ExpressionComparator; use crate::query::structs::expression::ExpressionResult::{Bool, Int, Str}; use crate::query::structs::token::{Token, TokenKind}; use crate::utils::errors::Errored; -use crate::utils::errors::Errored::{Column, Syntax}; +use crate::utils::errors::Errored::{Column, Default, Syntax}; use std::collections::HashMap; use std::fmt::{Debug, Formatter}; @@ -92,6 +92,16 @@ impl ExpressionNode { None => errored!(Column, "column {} does not exist", t.value), } } + + pub fn as_leaf_tuple(&self) -> Result<(&Token, &Token), Errored> { + match self { + ExpressionNode::Statement { left, right, .. } => match (&**left, &**right) { + (ExpressionNode::Leaf(l), ExpressionNode::Leaf(r)) => Ok((l, r)), + _ => errored!(Default, "both sides of expression must be leaf nodes."), + }, + _ => errored!(Default, "expected a statement, but got: {:?}", self), + } + } } impl Debug for ExpressionNode { diff --git a/src/query/structs/query.rs b/src/query/structs/query.rs index 48a53df..d461f4e 100644 --- a/src/query/structs/query.rs +++ b/src/query/structs/query.rs @@ -62,6 +62,13 @@ impl Display for Query { writeln!(f, " {:?}", values)?; } writeln!(f, "}} ")?; + writeln!(f, "Updates {{ ")?; + for up in &self.updates { + if let Ok((l, r)) = up.as_leaf_tuple() { + writeln!(f, " {} -> {}", l.value, r.value)?; + } + } + writeln!(f, "}} ")?; writeln!(f, "Updates: {:?}", self.updates)?; writeln!(f, "Conditions: {:?}", self.conditions)?; writeln!(f, "Ordering: {:?}", self.ordering) diff --git a/src/query/structs/row.rs b/src/query/structs/row.rs index c8c3574..eef9f3e 100644 --- a/src/query/structs/row.rs +++ b/src/query/structs/row.rs @@ -1,8 +1,8 @@ use crate::errored; -use crate::query::structs::expression::ExpressionResult; +use crate::query::structs::expression::{ExpressionNode, ExpressionResult}; use crate::query::structs::query::Query; use crate::utils::errors::Errored; -use crate::utils::errors::Errored::{Column, Syntax, Table}; +use crate::utils::errors::Errored::{Column, Default, Syntax, Table}; use std::collections::HashMap; pub struct Row<'a> { @@ -39,8 +39,16 @@ impl<'a> Row<'a> { Ok(()) } - pub fn update_value(&mut self, key: String, value: String) -> Result<(), Errored> { - self.insert(&key, value)?; + pub fn update_values(&mut self, updates: &Vec) -> Result<(), Errored> { + for up in updates { + if let Ok((field, value)) = up.as_leaf_tuple() { + let k = &field.value; + let v = &value.value; + self.insert(k, v.to_string())? + } else { + errored!(Default, "error while updating values.") + } + } Ok(()) } diff --git a/src/utils/errors.rs b/src/utils/errors.rs index efab932..4b27ef8 100644 --- a/src/utils/errors.rs +++ b/src/utils/errors.rs @@ -51,12 +51,12 @@ impl Display for Errored { impl From for Errored { fn from(value: io::Error) -> Self { - Default(format!("[IO]: {}", value)) + Default(format!("IO - {}", value)) } } impl From for Errored { fn from(value: ParseIntError) -> Self { - Default(format!("[PARSE_INT]: {}", value)) + Default(format!("PARSE_INT - {}", value)) } } diff --git a/src/utils/files.rs b/src/utils/files.rs index c97c0a4..0880021 100644 --- a/src/utils/files.rs +++ b/src/utils/files.rs @@ -2,11 +2,13 @@ use crate::errored; use crate::utils::errors::Errored; use crate::utils::errors::Errored::Default; use std::fs::File; +use std::hash::{DefaultHasher, Hash, Hasher}; use std::io::{BufRead, BufReader}; use std::path::{Path, PathBuf}; +use std::{fs, thread}; -const TEMP_EXTENSION: &str = ".tmp"; -const CSV_EXTENSION: &str = ".csv"; +const TEMP_EXTENSION: &str = "tmp"; +const CSV_EXTENSION: &str = "csv"; const CSV_SEPARATOR: &str = ","; pub fn extract_header(reader: &mut BufReader<&File>) -> Result, Errored> { @@ -21,20 +23,8 @@ pub fn split_csv(line: &str) -> Vec { .collect::>() } -pub fn validate_path(dir: &str) -> Result<&Path, Errored> { - let path = Path::new(dir); - if !path.exists() { - errored!(Default, "path '{dir}' does not exist"); - } else if !path.is_dir() { - errored!(Default, "path '{dir}' is not a valid directory"); - } else if path.read_dir()?.next().is_none() { - errored!(Default, "path '{dir}' is an empty directory"); - } - Ok(path) -} - pub fn get_table_path(dir_path: &Path, table_name: &str) -> Result { - let table_path = dir_path.join(format!("{}{}", table_name, CSV_EXTENSION)); + let table_path = dir_path.join(table_name).with_extension(CSV_EXTENSION); if !table_path.is_file() { errored!( Default, @@ -46,11 +36,46 @@ pub fn get_table_path(dir_path: &Path, table_name: &str) -> Result Result { - let table_path = table_path.with_extension(TEMP_EXTENSION); - Ok(File::create(table_path)?) +pub fn get_temp_file(table_name: &str, table_path: &Path) -> Result<(File, PathBuf), Errored> { + let id = thread::current().id(); + let mut hasher = DefaultHasher::new(); + id.hash(&mut hasher); + let table_path = table_path + .with_file_name(format!("{}_{}", table_name, hasher.finish())) + .with_extension(TEMP_EXTENSION); + Ok(( + File::options() + .create_new(true) + .read(true) + .write(true) + .truncate(true) + .open(&table_path)?, + table_path, + )) +} + +pub fn delete_temp_file(table_path: &Path, temp_path: &Path) -> Result<(), Errored> { + if let Some(ex) = temp_path.extension() { + if ex.to_string_lossy() != TEMP_EXTENSION { + errored!(Default, "tried to delete non_temporary file.") + } + } + fs::rename(temp_path, table_path)?; + Ok(()) } pub fn get_table_file(table_path: &Path) -> Result { Ok(File::open(table_path)?) } + +pub fn validate_path(dir: &str) -> Result<&Path, Errored> { + let path = Path::new(dir); + if !path.exists() { + errored!(Default, "path '{dir}' does not exist"); + } else if !path.is_dir() { + errored!(Default, "path '{dir}' is not a valid directory"); + } else if path.read_dir()?.next().is_none() { + errored!(Default, "path '{dir}' is an empty directory"); + } + Ok(path) +} diff --git a/tests/clientes.csv b/tests/tables/clientes.csv similarity index 100% rename from tests/clientes.csv rename to tests/tables/clientes.csv From da458818dc68709a209e81e4761fefbf2c78a3af Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 8 Sep 2024 17:23:06 -0300 Subject: [PATCH 056/103] first implementations of delete and insert --- src/query/executor/delete.rs | 24 +++++++++++++++++++++++- src/query/executor/insert.rs | 13 ++++++++++++- src/query/executor/update.rs | 3 +-- src/query/structs/expression.rs | 2 +- src/query/structs/row.rs | 7 ------- src/utils/files.rs | 2 +- tests/tables/ordenes.csv | 11 +++++++++++ 7 files changed, 49 insertions(+), 13 deletions(-) create mode 100644 tests/tables/ordenes.csv diff --git a/src/query/executor/delete.rs b/src/query/executor/delete.rs index 05b0c43..dd9de02 100644 --- a/src/query/executor/delete.rs +++ b/src/query/executor/delete.rs @@ -1,9 +1,31 @@ use crate::query::executor::Executor; +use crate::query::structs::row::Row; use crate::utils::errors::Errored; +use crate::utils::files::{ + delete_temp_file, extract_header, get_table_file, get_temp_file, split_csv, +}; +use std::io::{BufRead, BufReader, BufWriter, Write}; impl Executor { pub fn run_delete(&self) -> Result<(), Errored> { - //delete_temp_file(&self.table_path)?; + let table = get_table_file(&self.table_path)?; + let (temp_table, temp_path) = get_temp_file(&self.query.table, &self.table_path)?; + let mut reader = BufReader::new(&table); + let mut writer = BufWriter::new(temp_table); + let header = extract_header(&mut reader)?; + writeln!(writer, "{}", header.join(","))?; + for line in reader.lines() { + let l = line?; + let fields = split_csv(&l); + let mut row = Row::new(&header); + row.set_new_values(fields)?; + if row.matches_condition(&self.query)? { + continue; + } else { + writeln!(writer, "{}", l)? + } + } + delete_temp_file(&self.table_path, &temp_path)?; Ok(()) } } diff --git a/src/query/executor/insert.rs b/src/query/executor/insert.rs index b4448bf..b895510 100644 --- a/src/query/executor/insert.rs +++ b/src/query/executor/insert.rs @@ -1,9 +1,20 @@ use crate::query::executor::Executor; +use crate::query::structs::row::Row; use crate::utils::errors::Errored; +use crate::utils::files::{extract_header, get_table_file}; +use std::io::{BufReader, Write}; impl Executor { pub fn run_insert(&self) -> Result<(), Errored> { - //delete_temp_file(&self.table_path)?; + let mut table = get_table_file(&self.table_path)?; + let mut reader = BufReader::new(&table); + let header = extract_header(&mut reader)?; + for insert in &self.query.inserts { + let fields: Vec = insert.iter().map(|t| t.value.to_string()).collect(); + let mut row = Row::new(&header); + row.set_new_values(fields)?; + writeln!(table, "{}", row.as_csv_string())? + } Ok(()) } } diff --git a/src/query/executor/update.rs b/src/query/executor/update.rs index c0bc9d1..07c749b 100644 --- a/src/query/executor/update.rs +++ b/src/query/executor/update.rs @@ -1,5 +1,4 @@ use crate::query::executor::Executor; -use crate::query::structs::expression::ExpressionNode::Empty; use crate::query::structs::row::Row; use crate::utils::errors::Errored; use crate::utils::files::{ @@ -20,7 +19,7 @@ impl Executor { let fields = split_csv(&l); let mut row = Row::new(&header); row.set_new_values(fields)?; - if self.query.conditions == Empty || row.matches_condition(&self.query)? { + if row.matches_condition(&self.query)? { row.update_values(&self.query.updates)?; writeln!(writer, "{}", row.as_csv_string())? } else { diff --git a/src/query/structs/expression.rs b/src/query/structs/expression.rs index c096408..3b9d1eb 100644 --- a/src/query/structs/expression.rs +++ b/src/query/structs/expression.rs @@ -44,7 +44,7 @@ pub enum ExpressionResult { impl ExpressionNode { pub fn evaluate(&self, values: &HashMap) -> Result { match self { - ExpressionNode::Empty => Ok(Bool(false)), + ExpressionNode::Empty => Ok(Bool(true)), ExpressionNode::Leaf(t) => match t.kind { TokenKind::Identifier => ExpressionNode::get_variable_value(values, t), TokenKind::String => Ok(Str(t.value.to_string())), diff --git a/src/query/structs/row.rs b/src/query/structs/row.rs index eef9f3e..4d0c2c7 100644 --- a/src/query/structs/row.rs +++ b/src/query/structs/row.rs @@ -32,13 +32,6 @@ impl<'a> Row<'a> { Ok(()) } - pub fn clear(&mut self) -> Result<(), Errored> { - for key in self.header.iter() { - self.insert(key, "".to_string())?; - } - Ok(()) - } - pub fn update_values(&mut self, updates: &Vec) -> Result<(), Errored> { for up in updates { if let Ok((field, value)) = up.as_leaf_tuple() { diff --git a/src/utils/files.rs b/src/utils/files.rs index 0880021..04c4209 100644 --- a/src/utils/files.rs +++ b/src/utils/files.rs @@ -65,7 +65,7 @@ pub fn delete_temp_file(table_path: &Path, temp_path: &Path) -> Result<(), Error } pub fn get_table_file(table_path: &Path) -> Result { - Ok(File::open(table_path)?) + Ok(File::options().read(true).append(true).open(table_path)?) } pub fn validate_path(dir: &str) -> Result<&Path, Errored> { diff --git a/tests/tables/ordenes.csv b/tests/tables/ordenes.csv new file mode 100644 index 0000000..1ee9dbe --- /dev/null +++ b/tests/tables/ordenes.csv @@ -0,0 +1,11 @@ +id,id_cliente,producto,cantidad +101,1,Laptop,1 +103,1,Monitor,1 +102,2,Teléfono,2 +104,3,Teclado,1 +105,4,Mouse,2 +106,5,Impresora,1 +107,6,Altavoces,1 +108,4,Auriculares,1 +109,5,Laptop,1 +110,6,Teléfono,2 \ No newline at end of file From b2635a91e59cf135a98f3a8068761ed6d127accb Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 8 Sep 2024 17:45:56 -0300 Subject: [PATCH 057/103] polished insert implementation --- src/query/executor/delete.rs | 2 +- src/query/executor/insert.rs | 3 ++- src/query/executor/select.rs | 2 +- src/query/executor/update.rs | 4 ++-- src/query/structs/row.rs | 19 +++++++++++++++++-- tests/tables/ordenes.csv | 2 +- 6 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/query/executor/delete.rs b/src/query/executor/delete.rs index dd9de02..664f52f 100644 --- a/src/query/executor/delete.rs +++ b/src/query/executor/delete.rs @@ -18,7 +18,7 @@ impl Executor { let l = line?; let fields = split_csv(&l); let mut row = Row::new(&header); - row.set_new_values(fields)?; + row.read_new_values(fields)?; if row.matches_condition(&self.query)? { continue; } else { diff --git a/src/query/executor/insert.rs b/src/query/executor/insert.rs index b895510..16b0266 100644 --- a/src/query/executor/insert.rs +++ b/src/query/executor/insert.rs @@ -12,7 +12,8 @@ impl Executor { for insert in &self.query.inserts { let fields: Vec = insert.iter().map(|t| t.value.to_string()).collect(); let mut row = Row::new(&header); - row.set_new_values(fields)?; + row.clear()?; + row.insert_values(&self.query.columns, fields)?; writeln!(table, "{}", row.as_csv_string())? } Ok(()) diff --git a/src/query/executor/select.rs b/src/query/executor/select.rs index d9aaf20..a003eef 100644 --- a/src/query/executor/select.rs +++ b/src/query/executor/select.rs @@ -19,7 +19,7 @@ impl Executor { let l = line?; let fields = split_csv(&l); let mut row = Row::new(&header); - row.set_new_values(fields)?; + row.read_new_values(fields)?; if row.matches_condition(&self.query)? { matched_rows.push(row) } diff --git a/src/query/executor/update.rs b/src/query/executor/update.rs index 07c749b..22ffe85 100644 --- a/src/query/executor/update.rs +++ b/src/query/executor/update.rs @@ -18,9 +18,9 @@ impl Executor { let l = line?; let fields = split_csv(&l); let mut row = Row::new(&header); - row.set_new_values(fields)?; + row.read_new_values(fields)?; if row.matches_condition(&self.query)? { - row.update_values(&self.query.updates)?; + row.apply_updates(&self.query.updates)?; writeln!(writer, "{}", row.as_csv_string())? } else { writeln!(writer, "{}", l)? diff --git a/src/query/structs/row.rs b/src/query/structs/row.rs index 4d0c2c7..ddcabe1 100644 --- a/src/query/structs/row.rs +++ b/src/query/structs/row.rs @@ -1,6 +1,7 @@ use crate::errored; use crate::query::structs::expression::{ExpressionNode, ExpressionResult}; use crate::query::structs::query::Query; +use crate::query::structs::token::Token; use crate::utils::errors::Errored; use crate::utils::errors::Errored::{Column, Default, Syntax, Table}; use std::collections::HashMap; @@ -32,7 +33,14 @@ impl<'a> Row<'a> { Ok(()) } - pub fn update_values(&mut self, updates: &Vec) -> Result<(), Errored> { + pub fn clear(&mut self) -> Result<(), Errored> { + for key in self.header { + self.insert(key, "".to_string())? + } + Ok(()) + } + + pub fn apply_updates(&mut self, updates: &Vec) -> Result<(), Errored> { for up in updates { if let Ok((field, value)) = up.as_leaf_tuple() { let k = &field.value; @@ -45,7 +53,7 @@ impl<'a> Row<'a> { Ok(()) } - pub fn set_new_values(&mut self, values: Vec) -> Result<(), Errored> { + pub fn read_new_values(&mut self, values: Vec) -> Result<(), Errored> { if self.header.len() != values.len() { errored!( Table, @@ -60,6 +68,13 @@ impl<'a> Row<'a> { Ok(()) } + pub fn insert_values(&mut self, columns: &[Token], values: Vec) -> Result<(), Errored> { + for (col, value) in columns.iter().zip(values) { + self.insert(&col.value, value)? + } + Ok(()) + } + pub fn as_csv_string(&self) -> String { let mut fields: Vec<&str> = Vec::new(); for key in self.header { diff --git a/tests/tables/ordenes.csv b/tests/tables/ordenes.csv index 1ee9dbe..bf38946 100644 --- a/tests/tables/ordenes.csv +++ b/tests/tables/ordenes.csv @@ -8,4 +8,4 @@ id,id_cliente,producto,cantidad 107,6,Altavoces,1 108,4,Auriculares,1 109,5,Laptop,1 -110,6,Teléfono,2 \ No newline at end of file +110,6,Teléfono,2 From 692d272d026a644490c90240b5d76df466c6d9f5 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 8 Sep 2024 23:22:27 -0300 Subject: [PATCH 058/103] added a LOT of unit tests, (jesus christ im never doing that again) --- src/query/builder/delete.rs | 63 +++++++++++++ src/query/builder/expression.rs | 142 +++++++++++++++++++++++++++++ src/query/builder/insert.rs | 89 ++++++++++++++++++ src/query/builder/select.rs | 110 +++++++++++++++++++++++ src/query/builder/update.rs | 81 +++++++++++++++++ src/query/executor/delete.rs | 2 +- src/query/executor/select.rs | 2 +- src/query/executor/update.rs | 2 +- src/query/structs/comparator.rs | 134 +++++++++++++++++++++++++++ src/query/structs/expression.rs | 154 ++++++++++++++++++++++++++++++++ src/query/structs/ordering.rs | 7 +- src/query/structs/row.rs | 150 +++++++++++++++++++++++++++++-- src/query/tokenizer.rs | 134 ++++++++++++++++++++++++++- src/utils/files.rs | 81 +++++++++++++++++ 14 files changed, 1138 insertions(+), 13 deletions(-) diff --git a/src/query/builder/delete.rs b/src/query/builder/delete.rs index 2d0ace0..6b4ade7 100644 --- a/src/query/builder/delete.rs +++ b/src/query/builder/delete.rs @@ -41,3 +41,66 @@ impl Builder for DeleteBuilder { validate_keywords(ALLOWED_KEYWORDS, &self.tokens, Delete) } } + +#[cfg(test)] +mod tests { + use crate::query::structs::expression::ExpressionNode::Empty; + use crate::query::structs::operation::Operation::Delete; + use crate::query::structs::query::Query; + use crate::query::structs::token::{Token, TokenKind}; + use crate::query::tokenizer::Tokenizer; + + fn tokenize(sql: &str) -> Vec { + let mut tokenizer = Tokenizer::new(); + tokenizer.tokenize(sql).unwrap() + } + + fn to_token(value: &str, kind: TokenKind) -> Token { + Token { + value: value.to_string(), + kind, + } + } + + #[test] + fn test_delete_simple() { + let sql = "DELETE FROM ordenes"; + let tokens = tokenize(sql); + let query = Query::from(tokens).unwrap(); + + assert_eq!(query.operation, Delete); + assert_eq!(query.table, "ordenes"); + assert_eq!(query.conditions, Empty); + } + + #[test] + fn test_delete_with_conditions() { + let sql = "DELETE FROM ordenes WHERE id = 1"; + let tokens = tokenize(sql); + let query = Query::from(tokens).unwrap(); + + assert_eq!(query.operation, Delete); + assert_eq!(query.table, "ordenes"); + assert_ne!(query.conditions, Empty); + } + + #[test] + fn test_delete_invalid_keyword() { + let sql = "DELETE FROM ordenes ORDER BY id"; + let tokens = tokenize(sql); + let result = Query::from(tokens); + + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("ORDER BY")); + } + + #[test] + fn test_delete_missing_table() { + let sql = "DELETE WHERE id = 1"; + let tokens = tokenize(sql); + let result = Query::from(tokens); + + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("FROM")); + } +} diff --git a/src/query/builder/expression.rs b/src/query/builder/expression.rs index f8e075f..c214ee6 100644 --- a/src/query/builder/expression.rs +++ b/src/query/builder/expression.rs @@ -119,3 +119,145 @@ impl ExpressionBuilder { Ok(op) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::structs::token::TokenKind::*; + use crate::query::structs::token::{Token, TokenKind}; + use std::ops::Deref; + + fn create_token(kind: TokenKind, value: &str) -> Token { + Token { + kind, + value: value.to_string(), + } + } + + fn operator_should_be(node: &ExpressionNode, op: ExpressionOperator) { + match node { + ExpressionNode::Statement { operator, .. } => { + assert_eq!(*operator, op); + } + Empty => { + if op != None { + panic!("Expected a None operator but got {:?}", op) + } + } + _ => panic!("Expected an {:?} expression", op), + } + } + + fn leaves_should_have_op( + node: ExpressionNode, + l_op: ExpressionOperator, + r_op: ExpressionOperator, + ) { + match node { + ExpressionNode::Statement { left, right, .. } => { + operator_should_be(left.deref(), l_op); + operator_should_be(right.deref(), r_op); + } + _ => panic!("Expected an OR expression"), + } + } + + #[test] + fn test_parse_or_expression() { + let mut tokens = VecDeque::from(vec![ + create_token(Identifier, "x"), + create_token(Keyword, "="), + create_token(Number, "1"), + create_token(Keyword, "OR"), + create_token(Identifier, "y"), + create_token(Keyword, "="), + create_token(Number, "2"), + ]); + + let result = ExpressionBuilder::parse_expressions(&mut tokens).unwrap(); + operator_should_be(&result, Or); + leaves_should_have_op(result, Equals, Equals); + } + + #[test] + fn test_parse_and_expression() { + let mut tokens = VecDeque::from(vec![ + create_token(Identifier, "x"), + create_token(Keyword, "="), + create_token(Number, "1"), + create_token(Keyword, "AND"), + create_token(Identifier, "y"), + create_token(Keyword, "="), + create_token(Number, "2"), + ]); + + let result = ExpressionBuilder::parse_expressions(&mut tokens).unwrap(); + operator_should_be(&result, And); + leaves_should_have_op(result, Equals, Equals); + } + + #[test] + fn test_parse_not_expression() { + let mut tokens = VecDeque::from(vec![ + create_token(Keyword, "NOT"), + create_token(ParenthesisOpen, "("), + create_token(Identifier, "x"), + create_token(Keyword, "="), + create_token(Number, "1"), + create_token(ParenthesisClose, ")"), + ]); + + let result = ExpressionBuilder::parse_expressions(&mut tokens).unwrap(); + operator_should_be(&result, Not); + leaves_should_have_op(result, Equals, None) + } + + #[test] + fn test_parse_comparison_expression() { + let mut tokens = VecDeque::from(vec![ + create_token(Identifier, "x"), + create_token(Keyword, ">"), + create_token(Number, "10"), + ]); + + let result = ExpressionBuilder::parse_expressions(&mut tokens).unwrap(); + operator_should_be(&result, GreaterThan) + } + + #[test] + fn test_parse_complex_expression() { + let mut tokens = VecDeque::from(vec![ + create_token(Identifier, "x"), + create_token(Keyword, "="), + create_token(Number, "1"), + create_token(Keyword, "AND"), + create_token(ParenthesisOpen, "("), + create_token(Identifier, "y"), + create_token(Keyword, "="), + create_token(Number, "2"), + create_token(Keyword, "OR"), + create_token(Keyword, "NOT"), + create_token(Identifier, "z"), + create_token(Keyword, "="), + create_token(Number, "3"), + create_token(ParenthesisClose, ")"), + ]); + + let result = ExpressionBuilder::parse_expressions(&mut tokens).unwrap(); + operator_should_be(&result, And); + leaves_should_have_op(result, Equals, Or); + } + + #[test] + fn test_parse_invalid_token() { + let mut tokens = VecDeque::from(vec![ + create_token(Keyword, "INVALID"), + create_token(Identifier, "x"), + create_token(Keyword, "="), + create_token(Number, "1"), + ]); + + let result = ExpressionBuilder::parse_expressions(&mut tokens); + assert!(result.is_err()); + } +} diff --git a/src/query/builder/insert.rs b/src/query/builder/insert.rs index 0d9aa47..e9e5d6e 100644 --- a/src/query/builder/insert.rs +++ b/src/query/builder/insert.rs @@ -84,3 +84,92 @@ impl Builder for InsertBuilder { validate_keywords(ALLOWED_KEYWORDS, &self.tokens, Insert) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::structs::token::TokenKind::{Identifier, Number, String}; + use crate::query::tokenizer::Tokenizer; + + fn tokenize(sql: &str) -> Vec { + let mut tokenizer = Tokenizer::new(); + tokenizer.tokenize(sql).unwrap() + } + + fn to_token(value: &str, kind: TokenKind) -> Token { + Token { + value: value.to_string(), + kind, + } + } + + #[test] + fn test_insert_simple() { + let sql = "INSERT INTO ordenes (id, producto) VALUES (1, 'Laptop')"; + let tokens = tokenize(sql); + + let query = Query::from(tokens).unwrap(); + + assert_eq!(query.operation, Insert); + assert_eq!(query.table, "ordenes"); + assert_eq!( + query.columns, + vec![to_token("id", Identifier), to_token("producto", Identifier)] + ); + assert_eq!( + query.inserts, + vec![vec![to_token("1", Number), to_token("Laptop", String),]] + ); + } + + #[test] + fn test_insert_multiple_values() { + let sql = "INSERT INTO ordenes (id, producto) VALUES (1, 'Laptop'), (2, 'PS4');"; + let tokens = tokenize(sql); + let query = Query::from(tokens).unwrap(); + + assert_eq!(query.operation, Insert); + assert_eq!(query.table, "ordenes"); + assert_eq!( + query.columns, + vec![to_token("id", Identifier), to_token("producto", Identifier)] + ); + assert_eq!( + query.inserts, + vec![ + vec![to_token("1", Number), to_token("Laptop", String),], + vec![to_token("2", Number), to_token("PS4", String),] + ] + ); + } + + #[test] + fn test_insert_invalid_columns() { + let sql = "INSERT INTO ordenes (id, producto) VALUES (1)"; + let tokens = tokenize(sql); + let result = Query::from(tokens); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("expected 2 columns")); + } + + #[test] + fn test_insert_invalid_keyword() { + let sql = "INSERT INTO ordenes (id, producto) VALUES (1, 'LAPTOP') WHERE 1=1"; + let tokens = tokenize(sql); + let result = Query::from(tokens); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("WHERE")); + } + + #[test] + fn test_insert_invalid_missing_parenthesis() { + let sql = "INSERT INTO ordenes id, producto VALUES (1, 'LAPTOP')"; + let tokens = tokenize(sql); + let result = Query::from(tokens); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("expected")); + } +} diff --git a/src/query/builder/select.rs b/src/query/builder/select.rs index c9a20be..e36d2c7 100644 --- a/src/query/builder/select.rs +++ b/src/query/builder/select.rs @@ -72,3 +72,113 @@ impl Builder for SelectBuilder { validate_keywords(ALLOWED_KEYWORDS, &self.tokens, Select) } } + +#[cfg(test)] +mod tests { + use crate::query::structs::expression::ExpressionNode::Empty; + use crate::query::structs::operation::Operation::Select; + use crate::query::structs::ordering::OrderKind::{Asc, Desc}; + use crate::query::structs::ordering::Ordering; + use crate::query::structs::query::Query; + use crate::query::structs::token::TokenKind::Identifier; + use crate::query::structs::token::{Token, TokenKind}; + use crate::query::tokenizer::Tokenizer; + + fn tokenize(sql: &str) -> Vec { + let mut tokenizer = Tokenizer::new(); + tokenizer.tokenize(sql).unwrap() + } + + fn to_token(value: &str, kind: TokenKind) -> Token { + Token { + value: value.to_string(), + kind, + } + } + + #[test] + fn test_select_basic() { + let sql = "SELECT id, producto FROM ordenes"; + let tokens = tokenize(sql); + let query = Query::from(tokens).unwrap(); + + assert_eq!(query.operation, Select); + assert_eq!( + query.columns, + vec![to_token("id", Identifier), to_token("producto", Identifier),] + ); + assert_eq!(query.table, "ordenes"); + assert_eq!(query.conditions, Empty); + assert!(query.ordering.is_empty()); + } + + #[test] + fn test_select_with_conditions() { + let sql = "SELECT id, producto, cantidad FROM ordenes WHERE cantidad > 30"; + let tokens = tokenize(sql); + let query = Query::from(tokens).unwrap(); + + assert_eq!(query.operation, Select); + assert_eq!( + query.columns, + vec![ + to_token("id", Identifier), + to_token("producto", Identifier), + to_token("cantidad", Identifier), + ] + ); + assert_eq!(query.table, "ordenes"); + assert_ne!(query.conditions, Empty); + assert!(query.ordering.is_empty()); + } + + #[test] + fn test_select_with_ordering() { + let sql = "SELECT id, producto FROM ordenes ORDER BY id DESC, producto"; + let tokens = tokenize(sql); + let query = Query::from(tokens).unwrap(); + + assert_eq!(query.operation, Select); + assert_eq!( + query.columns, + vec![to_token("id", Identifier), to_token("producto", Identifier),] + ); + assert_eq!(query.table, "ordenes"); + assert_eq!(query.conditions, Empty); + assert_eq!(query.ordering.len(), 2); + assert_eq!( + query.ordering[0], + Ordering { + field: to_token("id", Identifier), + kind: Desc, + } + ); + assert_eq!( + query.ordering[1], + Ordering { + field: to_token("producto", Identifier), + kind: Asc, + } + ); + } + + #[test] + fn test_select_invalid_keyword() { + let sql = "SELECT id, name FROM users ORDER BY id DESC VALUES"; + let tokens = tokenize(sql); + let result = Query::from(tokens); + + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("VALUES")); + } + + #[test] + fn test_select_missing_from() { + let sql = "SELECT id, name users"; + let tokens = tokenize(sql); + let result = Query::from(tokens); + + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("FROM")); + } +} diff --git a/src/query/builder/update.rs b/src/query/builder/update.rs index 03a7829..e1fb887 100644 --- a/src/query/builder/update.rs +++ b/src/query/builder/update.rs @@ -67,3 +67,84 @@ impl Builder for UpdateBuilder { validate_keywords(ALLOWED_KEYWORDS, &self.tokens, Update) } } + +#[cfg(test)] +mod tests { + use crate::query::structs::expression::ExpressionNode::Empty; + use crate::query::structs::expression::{ExpressionNode, ExpressionOperator}; + use crate::query::structs::operation::Operation::Update; + use crate::query::structs::query::Query; + use crate::query::structs::token::TokenKind::{Identifier, Number}; + use crate::query::structs::token::{Token, TokenKind}; + use crate::query::tokenizer::Tokenizer; + + fn tokenize(sql: &str) -> Vec { + let mut tokenizer = Tokenizer::new(); + tokenizer.tokenize(sql).unwrap() + } + + fn to_token(value: &str, kind: TokenKind) -> Token { + Token { + value: value.to_string(), + kind, + } + } + + #[test] + fn test_update_simple() { + let sql = "UPDATE ordenes SET id = 5"; + let tokens = tokenize(sql); + let query = Query::from(tokens).unwrap(); + + assert_eq!(query.operation, Update); + assert_eq!(query.table, "ordenes"); + assert_eq!( + query.updates, + vec![ExpressionNode::Statement { + operator: ExpressionOperator::Equals, + left: Box::new(ExpressionNode::Leaf(to_token("id", Identifier))), + right: Box::new(ExpressionNode::Leaf(to_token("5", Number))), + }] + ); + assert_eq!(query.conditions, Empty); + } + + #[test] + fn test_update_with_conditions() { + let sql = "UPDATE ordenes SET cantidad = 5 WHERE id = 1"; + let tokens = tokenize(sql); + let query = Query::from(tokens).unwrap(); + + assert_eq!(query.operation, Update); + assert_eq!(query.table, "ordenes"); + assert_eq!( + query.updates, + vec![ExpressionNode::Statement { + operator: ExpressionOperator::Equals, + left: Box::new(ExpressionNode::Leaf(to_token("cantidad", Identifier))), + right: Box::new(ExpressionNode::Leaf(to_token("5", Number))), + }] + ); + assert_ne!(query.conditions, Empty); + } + + #[test] + fn test_update_invalid_keyword() { + let sql = "UPDATE ordenes SET quantity = 5 ORDER BY id"; + let tokens = tokenize(sql); + let result = Query::from(tokens); + + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("ORDER BY")); + } + + #[test] + fn test_update_missing_set() { + let sql = "UPDATE ordenes quantity = 5"; + let tokens = tokenize(sql); + let result = Query::from(tokens); + + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("SET")); + } +} diff --git a/src/query/executor/delete.rs b/src/query/executor/delete.rs index 664f52f..8512618 100644 --- a/src/query/executor/delete.rs +++ b/src/query/executor/delete.rs @@ -18,7 +18,7 @@ impl Executor { let l = line?; let fields = split_csv(&l); let mut row = Row::new(&header); - row.read_new_values(fields)?; + row.read_new_row(fields)?; if row.matches_condition(&self.query)? { continue; } else { diff --git a/src/query/executor/select.rs b/src/query/executor/select.rs index a003eef..2ac0744 100644 --- a/src/query/executor/select.rs +++ b/src/query/executor/select.rs @@ -19,7 +19,7 @@ impl Executor { let l = line?; let fields = split_csv(&l); let mut row = Row::new(&header); - row.read_new_values(fields)?; + row.read_new_row(fields)?; if row.matches_condition(&self.query)? { matched_rows.push(row) } diff --git a/src/query/executor/update.rs b/src/query/executor/update.rs index 22ffe85..08046cb 100644 --- a/src/query/executor/update.rs +++ b/src/query/executor/update.rs @@ -18,7 +18,7 @@ impl Executor { let l = line?; let fields = split_csv(&l); let mut row = Row::new(&header); - row.read_new_values(fields)?; + row.read_new_row(fields)?; if row.matches_condition(&self.query)? { row.apply_updates(&self.query.updates)?; writeln!(writer, "{}", row.as_csv_string())? diff --git a/src/query/structs/comparator.rs b/src/query/structs/comparator.rs index e759cfc..48ffa65 100644 --- a/src/query/structs/comparator.rs +++ b/src/query/structs/comparator.rs @@ -69,3 +69,137 @@ impl ExpressionComparator { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::structs::expression::ExpressionOperator::*; + use std::cmp::Ordering::*; + + #[test] + fn test_compare_ints() { + assert_eq!( + ExpressionComparator::compare_ints(5, 5, &Equals).unwrap(), + Bool(true) + ); + assert_eq!( + ExpressionComparator::compare_ints(5, 4, &NotEquals).unwrap(), + Bool(true) + ); + assert_eq!( + ExpressionComparator::compare_ints(5, 4, &GreaterThan).unwrap(), + Bool(true) + ); + assert_eq!( + ExpressionComparator::compare_ints(4, 5, &LessThan).unwrap(), + Bool(true) + ); + assert_eq!( + ExpressionComparator::compare_ints(5, 5, &GreaterOrEqual).unwrap(), + Bool(true) + ); + assert_eq!( + ExpressionComparator::compare_ints(4, 5, &LessOrEqual).unwrap(), + Bool(true) + ); + } + + #[test] + fn test_compare_ints_invalid() { + assert!(ExpressionComparator::compare_ints(5, 5, &And).is_err()); + } + + #[test] + fn test_compare_str() { + assert_eq!( + ExpressionComparator::compare_str("a", "a", &Equals).unwrap(), + Bool(true) + ); + assert_eq!( + ExpressionComparator::compare_str("a", "b", &NotEquals).unwrap(), + Bool(true) + ); + assert_eq!( + ExpressionComparator::compare_str("b", "a", &GreaterThan).unwrap(), + Bool(true) + ); + assert_eq!( + ExpressionComparator::compare_str("a", "b", &LessThan).unwrap(), + Bool(true) + ); + assert_eq!( + ExpressionComparator::compare_str("a", "a", &GreaterOrEqual).unwrap(), + Bool(true) + ); + assert_eq!( + ExpressionComparator::compare_str("a", "b", &LessOrEqual).unwrap(), + Bool(true) + ); + } + + #[test] + fn test_compare_str_invalid() { + assert!(ExpressionComparator::compare_str("a", "a", &And).is_err()); + } + + #[test] + fn test_compare_bools() { + assert_eq!( + ExpressionComparator::compare_bools(true, false, &And).unwrap(), + Bool(false) + ); + assert_eq!( + ExpressionComparator::compare_bools(true, false, &Or).unwrap(), + Bool(true) + ); + assert_eq!( + ExpressionComparator::compare_bools(true, false, &Not).unwrap(), + Bool(false) + ); + } + + #[test] + fn test_compare_bools_invalid() { + assert!(ExpressionComparator::compare_bools(true, false, &Equals).is_err()); + } + + #[test] + fn test_compare_ordering_ints() { + assert_eq!( + ExpressionComparator::compare_ordering(&Int(5), &Int(5)).unwrap(), + Equal + ); + assert_eq!( + ExpressionComparator::compare_ordering(&Int(5), &Int(4)).unwrap(), + Greater + ); + assert_eq!( + ExpressionComparator::compare_ordering(&Int(4), &Int(5)).unwrap(), + Less + ); + } + + #[test] + fn test_compare_ordering_strs() { + assert_eq!( + ExpressionComparator::compare_ordering(&Str("a".to_string()), &Str("a".to_string())) + .unwrap(), + Equal + ); + assert_eq!( + ExpressionComparator::compare_ordering(&Str("b".to_string()), &Str("a".to_string())) + .unwrap(), + Greater + ); + assert_eq!( + ExpressionComparator::compare_ordering(&Str("a".to_string()), &Str("b".to_string())) + .unwrap(), + Less + ); + } + + #[test] + fn test_compare_ordering_invalid() { + assert!(ExpressionComparator::compare_ordering(&Int(5), &Str("a".to_string())).is_err()); + } +} diff --git a/src/query/structs/expression.rs b/src/query/structs/expression.rs index 3b9d1eb..36bd6c7 100644 --- a/src/query/structs/expression.rs +++ b/src/query/structs/expression.rs @@ -119,3 +119,157 @@ impl Debug for ExpressionNode { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::structs::token::Token; + use crate::query::structs::token::TokenKind::*; + use std::collections::HashMap; + + #[test] + fn test_evaluate_empty_node() { + let node = ExpressionNode::Empty; + assert_eq!(node.evaluate(&HashMap::new()).unwrap(), Bool(true)); + } + + #[test] + fn test_evaluate_leaf_identifier() { + let mut values = HashMap::new(); + values.insert("id_cliente".to_string(), "123".to_string()); + let node = ExpressionNode::Leaf(Token { + kind: Identifier, + value: "id_cliente".to_string(), + }); + assert_eq!(node.evaluate(&values).unwrap(), Int(123)); + } + + #[test] + fn test_evaluate_leaf_string() { + let node = ExpressionNode::Leaf(Token { + kind: String, + value: "buenaaaaas".to_string(), + }); + assert_eq!( + node.evaluate(&HashMap::new()).unwrap(), + Str("buenaaaaas".to_string()) + ); + } + + #[test] + fn test_evaluate_leaf_number() { + let node = ExpressionNode::Leaf(Token { + kind: Number, + value: "360".to_string(), + }); + assert_eq!(node.evaluate(&HashMap::new()).unwrap(), Int(360)); + } + + #[test] + fn test_evaluate_invalid_token() { + let node = ExpressionNode::Leaf(Token { + kind: Keyword, + value: "".to_string(), + }); + assert_eq!(node.evaluate(&HashMap::new()).unwrap(), Bool(false)); + } + + #[test] + fn test_evaluate_statement_equal_int() { + let left = ExpressionNode::Leaf(Token { + kind: Number, + value: "360".to_string(), + }); + let right = ExpressionNode::Leaf(Token { + kind: Number, + value: "360".to_string(), + }); + let node = ExpressionNode::Statement { + operator: ExpressionOperator::Equals, + left: Box::new(left), + right: Box::new(right), + }; + assert_eq!(node.evaluate(&HashMap::new()).unwrap(), Bool(true)); + } + + #[test] + fn test_evaluate_statement_not_equal_str() { + let left = ExpressionNode::Leaf(Token { + kind: String, + value: "rust".to_string(), + }); + let right = ExpressionNode::Leaf(Token { + kind: String, + value: "gleam".to_string(), + }); + let node = ExpressionNode::Statement { + operator: ExpressionOperator::NotEquals, + left: Box::new(left), + right: Box::new(right), + }; + assert_eq!(node.evaluate(&HashMap::new()).unwrap(), Bool(true)); + } + + #[test] + fn test_get_variable_value_existing() { + let mut values = HashMap::new(); + values.insert("id_cliente".to_string(), "789".to_string()); + let token = Token { + kind: Identifier, + value: "id_cliente".to_string(), + }; + assert_eq!( + ExpressionNode::get_variable_value(&values, &token).unwrap(), + Int(789) + ); + } + + #[test] + fn test_get_variable_value_non_existing() { + let values = HashMap::new(); + let token = Token { + kind: Identifier, + value: "id".to_string(), + }; + assert!(ExpressionNode::get_variable_value(&values, &token).is_err()); + } + + #[test] + fn test_as_leaf_tuple_valid() { + let left = ExpressionNode::Leaf(Token { + kind: Identifier, + value: "id_cliente".to_string(), + }); + let right = ExpressionNode::Leaf(Token { + kind: Identifier, + value: "360".to_string(), + }); + let node = ExpressionNode::Statement { + operator: ExpressionOperator::Equals, + left: Box::new(left), + right: Box::new(right), + }; + let (l, r) = node.as_leaf_tuple().unwrap(); + assert_eq!(l.value, "id_cliente"); + assert_eq!(r.value, "360"); + } + + #[test] + fn test_as_leaf_tuple_invalid() { + let left = ExpressionNode::Statement { + operator: ExpressionOperator::Equals, + left: Box::new(ExpressionNode::Empty), + right: Box::new(ExpressionNode::Empty), + }; + let right = ExpressionNode::Leaf(Token { + kind: Identifier, + value: "col1".to_string(), + }); + let node = ExpressionNode::Statement { + operator: ExpressionOperator::Equals, + left: Box::new(left), + right: Box::new(right), + }; + assert!(node.as_leaf_tuple().is_err()); + } +} diff --git a/src/query/structs/ordering.rs b/src/query/structs/ordering.rs index e6ec10b..ca21822 100644 --- a/src/query/structs/ordering.rs +++ b/src/query/structs/ordering.rs @@ -2,12 +2,13 @@ use crate::query::structs::ordering::OrderKind::Asc; use crate::query::structs::token::Token; use std::fmt::{Debug, Formatter}; +#[derive(PartialEq)] pub struct Ordering { - pub(crate) field: Token, - pub(crate) kind: OrderKind, + pub field: Token, + pub kind: OrderKind, } -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum OrderKind { Asc, Desc, diff --git a/src/query/structs/row.rs b/src/query/structs/row.rs index ddcabe1..0ccf698 100644 --- a/src/query/structs/row.rs +++ b/src/query/structs/row.rs @@ -19,7 +19,7 @@ impl<'a> Row<'a> { } } - fn insert(&mut self, key: &str, value: String) -> Result<(), Errored> { + fn set(&mut self, key: &str, value: String) -> Result<(), Errored> { if self.header.contains(&key.to_string()) { self.values.insert(key.to_string(), value); } else { @@ -35,7 +35,7 @@ impl<'a> Row<'a> { pub fn clear(&mut self) -> Result<(), Errored> { for key in self.header { - self.insert(key, "".to_string())? + self.set(key, "".to_string())? } Ok(()) } @@ -45,7 +45,7 @@ impl<'a> Row<'a> { if let Ok((field, value)) = up.as_leaf_tuple() { let k = &field.value; let v = &value.value; - self.insert(k, v.to_string())? + self.set(k, v.to_string())? } else { errored!(Default, "error while updating values.") } @@ -53,7 +53,7 @@ impl<'a> Row<'a> { Ok(()) } - pub fn read_new_values(&mut self, values: Vec) -> Result<(), Errored> { + pub fn read_new_row(&mut self, values: Vec) -> Result<(), Errored> { if self.header.len() != values.len() { errored!( Table, @@ -63,14 +63,14 @@ impl<'a> Row<'a> { ); } for (key, value) in self.header.iter().zip(values) { - self.insert(key, value)?; + self.set(key, value)?; } Ok(()) } pub fn insert_values(&mut self, columns: &[Token], values: Vec) -> Result<(), Errored> { for (col, value) in columns.iter().zip(values) { - self.insert(&col.value, value)? + self.set(&col.value, value)? } Ok(()) } @@ -95,3 +95,141 @@ impl<'a> Row<'a> { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::structs::expression::{ExpressionNode, ExpressionOperator}; + use crate::query::structs::token::Token; + use crate::query::structs::token::TokenKind::*; + + #[test] + fn test_initializing() { + let header = vec!["id".to_string(), "apellido".to_string()]; + let row = Row::new(&header); + assert_eq!(row.header, &header); + assert_eq!(row.values.len(), 0); + } + + #[test] + fn test_insert_valid_column() { + let header = vec!["id".to_string(), "apellido".to_string()]; + let mut row = Row::new(&header); + assert!(row.set("id", "123".to_string()).is_ok()); + assert_eq!(row.values.get("id").unwrap(), "123"); + } + + #[test] + fn test_insert_invalid_column() { + let header = vec!["id".to_string(), "apellido".to_string()]; + let mut row = Row::new(&header); + assert!(row.set("nombre", "gabriel".to_string()).is_err()); + } + + #[test] + fn test_clear() { + let header = vec!["id".to_string(), "apellido".to_string()]; + let mut row = Row::new(&header); + row.set("id", "123".to_string()).unwrap(); + row.clear().unwrap(); + assert_eq!(row.values.get("id").unwrap(), ""); + assert_eq!(row.values.get("apellido").unwrap(), ""); + } + + #[test] + fn test_apply_updates() { + let header = vec!["id".to_string(), "apellido".to_string()]; + let mut row = Row::new(&header); + + let field = Token { + kind: Identifier, + value: "id".to_string(), + }; + let value = Token { + kind: String, + value: "360".to_string(), + }; + let update = ExpressionNode::Statement { + operator: ExpressionOperator::Equals, + left: Box::new(ExpressionNode::Leaf(field)), + right: Box::new(ExpressionNode::Leaf(value)), + }; + + row.apply_updates(&vec![update]).unwrap(); + assert_eq!(row.values.get("id").unwrap(), "360"); + } + + #[test] + fn test_read_new_values() { + let header = vec!["id".to_string(), "apellido".to_string()]; + let mut row = Row::new(&header); + let values = vec!["360".to_string(), "katta".to_string()]; + + row.read_new_row(values).unwrap(); + assert_eq!(row.values.get("id").unwrap(), "360"); + assert_eq!(row.values.get("apellido").unwrap(), "katta"); + } + + #[test] + fn test_read_new_values_mismatch_length() { + let header = vec!["id".to_string(), "apellido".to_string()]; + let mut row = Row::new(&header); + let values = vec!["365".to_string()]; // only one value instead of two + assert!(row.read_new_row(values).is_err()); + } + + #[test] + fn test_insert_values() { + let header = vec!["id".to_string(), "apellido".to_string()]; + let mut row = Row::new(&header); + let columns = vec![ + Token { + kind: Identifier, + value: "id".to_string(), + }, + Token { + kind: Identifier, + value: "apellido".to_string(), + }, + ]; + let values = vec!["360".to_string(), "katta".to_string()]; + + row.insert_values(&columns, values).unwrap(); + assert_eq!(row.values.get("id").unwrap(), "360"); + assert_eq!(row.values.get("apellido").unwrap(), "katta"); + } + + #[test] + fn test_as_csv_string() { + let header = vec!["id".to_string(), "apellido".to_string()]; + let mut row = Row::new(&header); + row.set("id", "360".to_string()).unwrap(); + row.set("apellido", "katta".to_string()).unwrap(); + + let csv_string = row.as_csv_string(); + assert_eq!(csv_string, "360,katta"); + } + + #[test] + fn test_matches_condition() { + let header = vec!["id".to_string()]; + let mut row = Row::new(&header); + row.set("id", "365".to_string()).unwrap(); + let condition = ExpressionNode::Statement { + operator: ExpressionOperator::GreaterThan, + left: Box::new(ExpressionNode::Leaf(Token { + kind: Identifier, + value: "id".to_string(), + })), + right: Box::new(ExpressionNode::Leaf(Token { + kind: Number, + value: "360".to_string(), + })), + }; + let query = Query { + conditions: condition, + ..Query::default() + }; + assert!(row.matches_condition(&query).unwrap()); + } +} diff --git a/src/query/tokenizer.rs b/src/query/tokenizer.rs index 030ba36..07063d2 100644 --- a/src/query/tokenizer.rs +++ b/src/query/tokenizer.rs @@ -192,11 +192,12 @@ impl Tokenizer { F: Fn(char) -> bool, { for t in tokens { + let t = t.to_uppercase(); let end = self.i + t.len(); if end <= sql.len() { let token = &sql[self.i..end]; let next_char = char_at(end, sql); - if token.to_uppercase() == t.to_uppercase() && !matches_kind(next_char) { + if token.to_uppercase() == t && !matches_kind(next_char) { return Some(token.to_uppercase()); } } @@ -247,3 +248,134 @@ fn is_identifier_char(c: char) -> bool { fn is_operator_char(c: char) -> bool { VALID_OPERATORS.contains(&c.to_string().as_str()) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::query::structs::token::TokenKind::{ + Identifier, Keyword, Number, Operator, ParenthesisClose, ParenthesisOpen, + String as TokenString, + }; + + #[test] + fn test_tokenize_select() { + let sql = "SELECT id, producto, id_cliente FROM ordenes WHERE cantidad > 1;"; + let mut tokenizer = Tokenizer::new(); + let tokens = tokenizer.tokenize(sql).unwrap(); + + assert_eq!(tokens.len(), 10); + assert_eq!(tokens[0].value, "SELECT"); + assert_eq!(tokens[0].kind, Keyword); + + assert_eq!(tokens[1].value, "id"); + assert_eq!(tokens[1].kind, Identifier); + + assert_eq!(tokens[2].value, "producto"); + assert_eq!(tokens[2].kind, Identifier); + + assert_eq!(tokens[3].value, "id_cliente"); + assert_eq!(tokens[3].kind, Identifier); + + assert_eq!(tokens[4].value, "FROM"); + assert_eq!(tokens[4].kind, Keyword); + + assert_eq!(tokens[5].value, "ordenes"); + assert_eq!(tokens[5].kind, Identifier); + + assert_eq!(tokens[6].value, "WHERE"); + assert_eq!(tokens[6].kind, Keyword); + + assert_eq!(tokens[7].value, "cantidad"); + assert_eq!(tokens[7].kind, Identifier); + + assert_eq!(tokens[8].value, ">"); + assert_eq!(tokens[8].kind, Operator); + + assert_eq!(tokens[9].value, "1"); + assert_eq!(tokens[9].kind, Number); + } + + #[test] + fn test_tokenize_select_with_parentheses() { + let sql = "SELECT id FROM t WHERE (a = 1)"; + let mut tokenizer = Tokenizer::new(); + let tokens = tokenizer.tokenize(sql).unwrap(); + + assert_eq!(tokens.len(), 10); + assert_eq!(tokens[0].value, "SELECT"); + assert_eq!(tokens[0].kind, Keyword); + + assert_eq!(tokens[1].value, "id"); + assert_eq!(tokens[1].kind, Identifier); + + assert_eq!(tokens[2].value, "FROM"); + assert_eq!(tokens[2].kind, Keyword); + + assert_eq!(tokens[3].value, "t"); + assert_eq!(tokens[3].kind, Identifier); + + assert_eq!(tokens[4].value, "WHERE"); + assert_eq!(tokens[4].kind, Keyword); + + assert_eq!(tokens[5].value, "("); + assert_eq!(tokens[5].kind, ParenthesisOpen); + + assert_eq!(tokens[6].value, "a"); + assert_eq!(tokens[6].kind, Identifier); + + assert_eq!(tokens[7].value, "="); + assert_eq!(tokens[7].kind, Operator); + + assert_eq!(tokens[8].value, "1"); + assert_eq!(tokens[8].kind, Number); + + assert_eq!(tokens[9].value, ")"); + assert_eq!(tokens[9].kind, ParenthesisClose); + } + + #[test] + fn test_tokenize_string_literals() { + let sql = "SELECT name FROM users WHERE name = 'Alice'"; + let mut tokenizer = Tokenizer::new(); + let tokens = tokenizer.tokenize(sql).unwrap(); + assert_eq!(tokens.len(), 8); + assert_eq!(tokens[7].value, "Alice"); + assert_eq!(tokens[7].kind, TokenString); + } + + #[test] + fn test_unclosed_parenthesis_error() { + let sql = "SELECT id FROM ordenes WHERE (producto = 'Laptop'"; + let mut tokenizer = Tokenizer::new(); + let result = tokenizer.tokenize(sql); + assert!(result.is_err()); + if let Err(e) = result { + assert!(e.to_string().contains("unclosed parentheses found.")); + } + } + + #[test] + fn test_unrecognized_char_error() { + let sql = "SELECT * FROM users WHERE age = @30"; + let mut tokenizer = Tokenizer::new(); + let result = tokenizer.tokenize(sql); + + assert!(result.is_err()); + if let Err(e) = result { + assert!(e.to_string().contains("could not tokenize char:")); + } + } + + #[test] + fn test_tokenize_with_operators() { + let sql = "SELECT * FROM users WHERE age >= 30"; + let mut tokenizer = Tokenizer::new(); + let tokens = tokenizer.tokenize(sql).unwrap(); + + assert_eq!(tokens.len(), 8); + assert_eq!(tokens[1].value, "*"); + assert_eq!(tokens[1].kind, Operator); + assert_eq!(tokens[6].value, ">="); + assert_eq!(tokens[6].kind, Operator); + } +} diff --git a/src/utils/files.rs b/src/utils/files.rs index 04c4209..3c3f2f3 100644 --- a/src/utils/files.rs +++ b/src/utils/files.rs @@ -79,3 +79,84 @@ pub fn validate_path(dir: &str) -> Result<&Path, Errored> { } Ok(path) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extract_header() { + let file = File::open("tests/tables/ordenes.csv").unwrap(); + let mut reader = BufReader::new(&file); + let header = extract_header(&mut reader).unwrap(); + assert_eq!(header, vec!["id", "id_cliente", "producto", "cantidad"]); + } + + #[test] + fn test_split_csv() { + let line = "id, id_cliente , email "; + let result = split_csv(line); + assert_eq!(result, vec!["id", "id_cliente", "email"]); + } + + #[test] + fn test_get_bad_table_path() { + let dir = Path::new("/dir/sin_tablas"); + let table_name = "ordenes"; + let result = get_table_path(dir, table_name); + assert!(result.is_err()); + } + + #[test] + fn test_get_temp_file() { + let table_name = "ordenes"; + let table_path = Path::new("tests/tables"); + let result = get_temp_file(table_name, table_path); + assert!(result.is_ok()); + let (_, temp_path) = result.unwrap(); + fs::remove_file(temp_path).unwrap(); + } + + #[test] + fn test_delete_temp_file() { + let table_path = Path::new("tests/tables/ordenes.csv"); + let (_, t_path) = get_temp_file("ordenes", table_path).unwrap(); + fs::copy(table_path, &t_path).unwrap(); + let result = delete_temp_file(table_path, &t_path); + assert!(result.is_ok()); + } + + #[test] + fn test_delete_non_temporary_file_error() { + let table_path = Path::new("tests/tablas/ordenes.csv"); + let temp_path = Path::new("tests/tablas/clientes.csv"); + let result = delete_temp_file(table_path, temp_path); + assert!(result.is_err()); + } + + #[test] + fn test_get_bad_table_file() { + let table_path = Path::new("/dir/tablas/no_existo.csv"); + let result = get_table_file(table_path); + assert!(result.is_err()); + } + + #[test] + fn test_validate_path_success() { + let path = Path::new("tests/tables"); + let result = validate_path(path.to_str().unwrap()); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_path_not_exist() { + let result = validate_path("/dir/sin_tablas/"); + assert!(result.is_err()); + } + + #[test] + fn test_validate_path_not_a_directory() { + let result = validate_path("/test/tables/clientes.csv"); + assert!(result.is_err()); + } +} From f1d0ced8b8fa97945f7efd61604e52a9e5f27415 Mon Sep 17 00:00:00 2001 From: katta <102127372+gabokatta@users.noreply.github.com> Date: Sun, 8 Sep 2024 23:29:09 -0300 Subject: [PATCH 059/103] Create rust.yml --- .github/workflows/rust.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/rust.yml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..9fd45e0 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,22 @@ +name: Rust + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose From 8c671699c170f747effafccd58ca26b14f53bebc Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 8 Sep 2024 23:30:14 -0300 Subject: [PATCH 060/103] some file changes --- src/utils/files.rs | 16 ++++++++-------- tests/{tables => unit_tables}/clientes.csv | 0 tests/{tables => unit_tables}/ordenes.csv | 0 3 files changed, 8 insertions(+), 8 deletions(-) rename tests/{tables => unit_tables}/clientes.csv (100%) rename tests/{tables => unit_tables}/ordenes.csv (100%) diff --git a/src/utils/files.rs b/src/utils/files.rs index 3c3f2f3..993f66d 100644 --- a/src/utils/files.rs +++ b/src/utils/files.rs @@ -86,7 +86,7 @@ mod tests { #[test] fn test_extract_header() { - let file = File::open("tests/tables/ordenes.csv").unwrap(); + let file = File::open("tests/unit_tables/ordenes.csv").unwrap(); let mut reader = BufReader::new(&file); let header = extract_header(&mut reader).unwrap(); assert_eq!(header, vec!["id", "id_cliente", "producto", "cantidad"]); @@ -101,7 +101,7 @@ mod tests { #[test] fn test_get_bad_table_path() { - let dir = Path::new("/dir/sin_tablas"); + let dir = Path::new("/dir/sin_unit_tables"); let table_name = "ordenes"; let result = get_table_path(dir, table_name); assert!(result.is_err()); @@ -119,7 +119,7 @@ mod tests { #[test] fn test_delete_temp_file() { - let table_path = Path::new("tests/tables/ordenes.csv"); + let table_path = Path::new("tests/unit_tables/ordenes.csv"); let (_, t_path) = get_temp_file("ordenes", table_path).unwrap(); fs::copy(table_path, &t_path).unwrap(); let result = delete_temp_file(table_path, &t_path); @@ -128,29 +128,29 @@ mod tests { #[test] fn test_delete_non_temporary_file_error() { - let table_path = Path::new("tests/tablas/ordenes.csv"); - let temp_path = Path::new("tests/tablas/clientes.csv"); + let table_path = Path::new("tests/unit_tables/ordenes.csv"); + let temp_path = Path::new("tests/unit_tables/clientes.csv"); let result = delete_temp_file(table_path, temp_path); assert!(result.is_err()); } #[test] fn test_get_bad_table_file() { - let table_path = Path::new("/dir/tablas/no_existo.csv"); + let table_path = Path::new("/dir/unit_tables/no_existo.csv"); let result = get_table_file(table_path); assert!(result.is_err()); } #[test] fn test_validate_path_success() { - let path = Path::new("tests/tables"); + let path = Path::new("tests/unit_tables"); let result = validate_path(path.to_str().unwrap()); assert!(result.is_ok()); } #[test] fn test_validate_path_not_exist() { - let result = validate_path("/dir/sin_tablas/"); + let result = validate_path("/dir/sin_unit_tables/"); assert!(result.is_err()); } diff --git a/tests/tables/clientes.csv b/tests/unit_tables/clientes.csv similarity index 100% rename from tests/tables/clientes.csv rename to tests/unit_tables/clientes.csv diff --git a/tests/tables/ordenes.csv b/tests/unit_tables/ordenes.csv similarity index 100% rename from tests/tables/ordenes.csv rename to tests/unit_tables/ordenes.csv From 8c35496e75294173cd8e2bcb855099fe16043938 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 8 Sep 2024 23:39:26 -0300 Subject: [PATCH 061/103] testing new CI --- .github/workflows/rust.yml | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9fd45e0..ff49860 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -15,8 +15,21 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose + - uses: actions/checkout@v4 + + - name: Install Rust + uses: actions/setup-rust@v1 + with: + rust-version: 'latest' + + - name: Format Code + run: cargo fmt -- --check + + - name: Build + run: cargo build --verbose + + - name: Run Clippy + run: cargo clippy --all-targets --all-features -- -D warnings + + - name: Run tests + run: cargo test --verbose From a9d3cb3c96cefd083ffb571f1d8cba3362e3f89f Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 8 Sep 2024 23:42:34 -0300 Subject: [PATCH 062/103] testing new CI pt2 --- .github/workflows/rust.yml | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ff49860..926a629 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,35 +1,22 @@ name: Rust - on: push: branches: [ "main" ] pull_request: branches: [ "main" ] - env: CARGO_TERM_COLOR: always - jobs: build: - runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - name: Install Rust - uses: actions/setup-rust@v1 - with: - rust-version: 'latest' - - name: Format Code run: cargo fmt -- --check - - name: Build run: cargo build --verbose - - - name: Run Clippy - run: cargo clippy --all-targets --all-features -- -D warnings - + - name: Build + run: cargo build --verbose - name: Run tests - run: cargo test --verbose + run: cargo test --verbose \ No newline at end of file From 657da68709fb72056f08f8ae6705e6b9589b5161 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 8 Sep 2024 23:45:48 -0300 Subject: [PATCH 063/103] testing new CI pt3 --- .github/workflows/rust.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 926a629..c8292ff 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -16,7 +16,7 @@ jobs: run: cargo fmt -- --check - name: Build run: cargo build --verbose - - name: Build - run: cargo build --verbose + - name: Run Clippy + run: cargo clippy --all-targets --all-features -- -D warnings - name: Run tests run: cargo test --verbose \ No newline at end of file From 7ed40dc0007d3ac61666ba3628215304ba3f8417 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 8 Sep 2024 23:47:57 -0300 Subject: [PATCH 064/103] removed unused methods in tests --- src/query/builder/delete.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/query/builder/delete.rs b/src/query/builder/delete.rs index 6b4ade7..05b41cc 100644 --- a/src/query/builder/delete.rs +++ b/src/query/builder/delete.rs @@ -55,13 +55,6 @@ mod tests { tokenizer.tokenize(sql).unwrap() } - fn to_token(value: &str, kind: TokenKind) -> Token { - Token { - value: value.to_string(), - kind, - } - } - #[test] fn test_delete_simple() { let sql = "DELETE FROM ordenes"; From 60b03f9f0d3defedf8eb08d1dd825d203e833a37 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Sun, 8 Sep 2024 23:52:24 -0300 Subject: [PATCH 065/103] fixed clippy --- README.md | 2 ++ src/query/builder/delete.rs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e985180..75495b5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # rustic-sql 🦀 +![workflow TP](https://github.com/gabokatta/rustic-sql/actions/workflows/rust.yml/badge.svg) + > woooo! diff --git a/src/query/builder/delete.rs b/src/query/builder/delete.rs index 05b41cc..49fd1ff 100644 --- a/src/query/builder/delete.rs +++ b/src/query/builder/delete.rs @@ -47,7 +47,7 @@ mod tests { use crate::query::structs::expression::ExpressionNode::Empty; use crate::query::structs::operation::Operation::Delete; use crate::query::structs::query::Query; - use crate::query::structs::token::{Token, TokenKind}; + use crate::query::structs::token::Token; use crate::query::tokenizer::Tokenizer; fn tokenize(sql: &str) -> Vec { From c7a971c3c8af5f927ef9d180ed9d7f70ca9eeff7 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 02:05:00 -0300 Subject: [PATCH 066/103] setup integration tests --- Cargo.lock | 9 ----- Cargo.toml | 3 -- src/query/builder/select.rs | 6 ++-- src/query/structs/ordering.rs | 4 +-- src/query/structs/query.rs | 26 +++++++------- src/query/structs/token.rs | 4 +-- src/query/tokenizer.rs | 3 ++ tests/integration.rs | 10 ++++++ tests/integration_tables/orders.csv | 11 ++++++ tests/integration_tables/users.csv | 11 ++++++ tests/utils/mod.rs | 54 +++++++++++++++++++++++++++++ 11 files changed, 111 insertions(+), 30 deletions(-) create mode 100644 tests/integration_tables/orders.csv create mode 100644 tests/integration_tables/users.csv create mode 100644 tests/utils/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 24da147..1de2011 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "log" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" - [[package]] name = "rustic-sql" version = "0.1.0" -dependencies = [ - "log", -] diff --git a/Cargo.toml b/Cargo.toml index 59b688b..3ea88fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,3 @@ name = "rustic-sql" version = "0.1.0" edition = "2021" - -[dependencies] -log = "0.4.22" diff --git a/src/query/builder/select.rs b/src/query/builder/select.rs index e36d2c7..8bd9023 100644 --- a/src/query/builder/select.rs +++ b/src/query/builder/select.rs @@ -27,8 +27,10 @@ impl SelectBuilder { if t.kind != Identifier { unexpected_token_in_stage("ORDER_BY", &t)? } - let mut new_order = Ordering::default(); - new_order.field = t; + let mut new_order = Ordering { + field: t, + ..Ordering::default() + }; if let Some(next) = self.tokens.front() { match next.kind { Keyword if next.value == "ASC" || next.value == "DESC" => { diff --git a/src/query/structs/ordering.rs b/src/query/structs/ordering.rs index ca21822..b1b278d 100644 --- a/src/query/structs/ordering.rs +++ b/src/query/structs/ordering.rs @@ -14,8 +14,8 @@ pub enum OrderKind { Desc, } -impl Ordering { - pub fn default() -> Self { +impl Default for Ordering { + fn default() -> Self { Self { field: Token::default(), kind: Asc, diff --git a/src/query/structs/query.rs b/src/query/structs/query.rs index d461f4e..5ffd6ba 100644 --- a/src/query/structs/query.rs +++ b/src/query/structs/query.rs @@ -25,18 +25,6 @@ pub struct Query { } impl Query { - pub fn default() -> Self { - Self { - operation: Unknown, - table: "".to_string(), - columns: vec![], - inserts: vec![], - updates: vec![], - conditions: ExpressionNode::default(), - ordering: vec![], - } - } - pub fn from(tokens: Vec) -> Result { let mut tokens = VecDeque::from(tokens); let kind = get_kind(tokens.pop_front()); @@ -50,6 +38,20 @@ impl Query { } } +impl Default for Query { + fn default() -> Self { + Self { + operation: Unknown, + table: "".to_string(), + columns: vec![], + inserts: vec![], + updates: vec![], + conditions: ExpressionNode::default(), + ordering: vec![], + } + } +} + impl Display for Query { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let fields: Vec<&str> = self.columns.iter().map(|f| f.value.as_str()).collect(); diff --git a/src/query/structs/token.rs b/src/query/structs/token.rs index 969eba3..e23bbf3 100644 --- a/src/query/structs/token.rs +++ b/src/query/structs/token.rs @@ -18,8 +18,8 @@ pub enum TokenKind { Keyword, } -impl Token { - pub fn default() -> Self { +impl Default for Token { + fn default() -> Self { Self { value: String::new(), kind: Unknown, diff --git a/src/query/tokenizer.rs b/src/query/tokenizer.rs index 07063d2..ccfa045 100644 --- a/src/query/tokenizer.rs +++ b/src/query/tokenizer.rs @@ -28,13 +28,16 @@ const RESERVED_KEYWORDS: &[&str] = &[ "NOT", ]; +#[derive(Default)] pub struct Tokenizer { i: usize, state: TokenizerState, parenthesis_count: i8, } +#[derive(Default)] enum TokenizerState { + #[default] Begin, IdentifierOrKeyword, Operator, diff --git a/tests/integration.rs b/tests/integration.rs index 8b13789..5aaa748 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1 +1,11 @@ +use crate::utils::RusticSQLTest; +mod utils; + +#[test] +fn test_empty_query() { + let test = RusticSQLTest::new(); + let result = test.run_with_output("".to_string()); + let output = String::from_utf8(result.stdout).unwrap(); + assert!(output.contains("empty")) +} diff --git a/tests/integration_tables/orders.csv b/tests/integration_tables/orders.csv new file mode 100644 index 0000000..dc141a3 --- /dev/null +++ b/tests/integration_tables/orders.csv @@ -0,0 +1,11 @@ +order_id,customer_name,product_name,quantity,price_per_unit +1001,John Doe,Laptop,1,999 +1002,Jane Smith,Smartphone,2,499 +1003,Alice Johnson,Headphones,3,149 +1004,Bob Brown,Keyboard,1,89 +1005,Charlie Davis,Mouse,2,29 +1006,David Wilson,Monitor,1,279 +1007,Eve Adams,Desk Chair,1,159 +1008,Frank Miller,Webcam,1,89 +1009,Grace Lee,USB Drive,5,15 +1010,Henry Clark,External Hard Drive,1,119 diff --git a/tests/integration_tables/users.csv b/tests/integration_tables/users.csv new file mode 100644 index 0000000..88f538c --- /dev/null +++ b/tests/integration_tables/users.csv @@ -0,0 +1,11 @@ +user_id,name,email,age +1,John Doe,john.doe@example.com,28 +2,Jane Smith,jane.smith@example.com,34 +3,Alice Johnson,alice.johnson@example.com,29 +4,Bob Brown,bob.brown@example.com,45 +5,Charlie Davis,charlie.davis@example.com,31 +6,David Wilson,david.wilson@example.com,27 +7,Eve Adams,eve.adams@example.com,33 +8,Frank Miller,frank.miller@example.com,40 +9,Grace Lee,grace.lee@example.com,25 +10,Henry Clark,henry.clark@example.com,38 diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs new file mode 100644 index 0000000..e089777 --- /dev/null +++ b/tests/utils/mod.rs @@ -0,0 +1,54 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; + +pub struct RusticSQLTest { + temp_dir: PathBuf, +} + +impl RusticSQLTest { + pub fn new() -> Self { + let temp_dir = Path::new("./tests/temp_tables"); + if temp_dir.exists() { + fs::remove_dir_all(temp_dir).expect("failed to clean previous test tables."); + } + fs::create_dir(temp_dir).expect("failed to create temp table directory."); + + let og_tables_path = Path::new("./tests/integration_tables"); + let orders = og_tables_path.join("orders.csv"); + let users = og_tables_path.join("users.csv"); + + let temp_orders = temp_dir.join("orders.csv"); + let temp_users = temp_dir.join("users.csv"); + + fs::copy(orders, &temp_orders).expect("failed to copy order table."); + fs::copy(users, &temp_users).expect("failed to copy user table."); + + RusticSQLTest { + temp_dir: temp_dir.to_path_buf(), + } + } + + pub fn args_for(&self, query: String) -> Vec { + vec![ + "target/debug/rustic-sql".to_string(), + self.temp_dir.to_str().unwrap().to_string(), + query, + ] + } + + pub fn run_with_output(&self, query: String) -> Output { + let args = self.args_for(query); + Command::new(&args[0]) + .arg(&args[1]) + .arg(&args[2]) + .output() + .unwrap() + } +} + +impl Drop for RusticSQLTest { + fn drop(&mut self) { + fs::remove_dir_all(&self.temp_dir).expect("Failed to clean up test directory"); + } +} From 1c9a11eea00cb342b0978727994e33045910a7c6 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 02:43:23 -0300 Subject: [PATCH 067/103] separated run into lib.rs --- src/lib.rs | 27 +++++++++++++++++++++++++++ src/main.rs | 28 +--------------------------- tests/integration.rs | 13 ++++++++++--- tests/utils/mod.rs | 15 +++++++++++---- 4 files changed, 49 insertions(+), 34 deletions(-) create mode 100644 src/lib.rs diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..81d89f2 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,27 @@ +use crate::query::executor::Executor; +use crate::query::structs::query::Query; +use crate::query::tokenizer::Tokenizer; +use crate::query::validate_query_string; +use crate::utils::files::validate_path; +use std::error::Error; + +pub mod query; +pub mod utils; + +pub fn run(args: Vec) -> Result<(), Box> { + if args.len() != 3 { + println!("invalid usage of rustic-sql"); + return Err("usage: cargo run -- ".into()); + } + + let path: &String = &args[1]; + let query: &String = &args[2]; + validate_path(path)?; + validate_query_string(query)?; + + let tokens = Tokenizer::new().tokenize(query)?; + let query = Query::from(tokens)?; + println!("\n{:?}", &query); + Executor::run(path, query)?; + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index dfbce2e..a343add 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,5 @@ -use crate::query::executor::Executor; -use crate::query::structs::query::Query; -use crate::query::tokenizer::Tokenizer; -use crate::utils::files::validate_path; -use query::validate_query_string; +use rustic_sql::run; use std::env; -use std::error::Error; - -mod query; -mod utils; fn main() { let args = env::args().collect(); @@ -15,21 +7,3 @@ fn main() { println!("{}", e); } } - -fn run(args: Vec) -> Result<(), Box> { - if args.len() != 3 { - println!("invalid usage of rustic-sql"); - return Err("usage: cargo run -- ".into()); - } - - let path: &String = &args[1]; - let query: &String = &args[2]; - validate_path(path)?; - validate_query_string(query)?; - - let tokens = Tokenizer::new().tokenize(query)?; - let query = Query::from(tokens)?; - println!("\n{:?}", &query); - Executor::run(path, query)?; - Ok(()) -} diff --git a/tests/integration.rs b/tests/integration.rs index 5aaa748..d6c6c96 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -5,7 +5,14 @@ mod utils; #[test] fn test_empty_query() { let test = RusticSQLTest::new(); - let result = test.run_with_output("".to_string()); - let output = String::from_utf8(result.stdout).unwrap(); - assert!(output.contains("empty")) + let result = test.run_for("".to_string()); + assert!(result.is_err_and(|e| e.to_string().contains("empty"))); +} + +#[test] +fn test_select_no_where() { + let test = RusticSQLTest::new(); + let query = "SELECT * FROM users"; + let result = test.run_with_output(query.to_string()); + println!("{}", result) } diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs index e089777..23682f2 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -1,6 +1,8 @@ +use rustic_sql::run; +use std::error::Error; use std::fs; use std::path::{Path, PathBuf}; -use std::process::{Command, Output}; +use std::process::Command; pub struct RusticSQLTest { temp_dir: PathBuf, @@ -37,13 +39,18 @@ impl RusticSQLTest { ] } - pub fn run_with_output(&self, query: String) -> Output { + pub fn run_for(&self, query: String) -> Result<(), Box> { + run(self.args_for(query)) + } + + pub fn run_with_output(&self, query: String) -> String { let args = self.args_for(query); - Command::new(&args[0]) + let output =Command::new(&args[0]) .arg(&args[1]) .arg(&args[2]) .output() - .unwrap() + .unwrap(); + String::from_utf8(output.stdout).unwrap() } } From b4c6a1811fe63ba6be042e082181bc127d603ba9 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 03:12:41 -0300 Subject: [PATCH 068/103] new test utils --- src/lib.rs | 1 - src/utils/files.rs | 9 +++++++-- tests/integration.rs | 5 +++-- tests/integration_tables/orders.csv | 2 +- tests/integration_tables/users.csv | 2 +- tests/utils/mod.rs | 20 +++++++++++++++----- 6 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 81d89f2..6991bc4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,6 @@ pub fn run(args: Vec) -> Result<(), Box> { let tokens = Tokenizer::new().tokenize(query)?; let query = Query::from(tokens)?; - println!("\n{:?}", &query); Executor::run(path, query)?; Ok(()) } diff --git a/src/utils/files.rs b/src/utils/files.rs index 993f66d..296df3d 100644 --- a/src/utils/files.rs +++ b/src/utils/files.rs @@ -36,12 +36,17 @@ pub fn get_table_path(dir_path: &Path, table_name: &str) -> Result Result<(File, PathBuf), Errored> { +pub fn get_temp_id() -> u64 { let id = thread::current().id(); let mut hasher = DefaultHasher::new(); id.hash(&mut hasher); + hasher.finish() +} + +pub fn get_temp_file(table_name: &str, table_path: &Path) -> Result<(File, PathBuf), Errored> { + let id = get_temp_id(); let table_path = table_path - .with_file_name(format!("{}_{}", table_name, hasher.finish())) + .with_file_name(format!("{}_{}", table_name, id)) .with_extension(TEMP_EXTENSION); Ok(( File::options() diff --git a/tests/integration.rs b/tests/integration.rs index d6c6c96..7954186 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -13,6 +13,7 @@ fn test_empty_query() { fn test_select_no_where() { let test = RusticSQLTest::new(); let query = "SELECT * FROM users"; - let result = test.run_with_output(query.to_string()); - println!("{}", result) + let expected_rows = 11; //with header + let result = test.run_and_get_rows(query.to_string()); + assert_eq!(expected_rows, result.len()); } diff --git a/tests/integration_tables/orders.csv b/tests/integration_tables/orders.csv index dc141a3..0c5ac02 100644 --- a/tests/integration_tables/orders.csv +++ b/tests/integration_tables/orders.csv @@ -8,4 +8,4 @@ order_id,customer_name,product_name,quantity,price_per_unit 1007,Eve Adams,Desk Chair,1,159 1008,Frank Miller,Webcam,1,89 1009,Grace Lee,USB Drive,5,15 -1010,Henry Clark,External Hard Drive,1,119 +1010,Henry Clark,External Hard Drive,1,119 \ No newline at end of file diff --git a/tests/integration_tables/users.csv b/tests/integration_tables/users.csv index 88f538c..d4cb26d 100644 --- a/tests/integration_tables/users.csv +++ b/tests/integration_tables/users.csv @@ -8,4 +8,4 @@ user_id,name,email,age 7,Eve Adams,eve.adams@example.com,33 8,Frank Miller,frank.miller@example.com,40 9,Grace Lee,grace.lee@example.com,25 -10,Henry Clark,henry.clark@example.com,38 +10,Henry Clark,henry.clark@example.com,38 \ No newline at end of file diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs index 23682f2..f8c9fda 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -1,4 +1,5 @@ use rustic_sql::run; +use rustic_sql::utils::files::get_temp_id; use std::error::Error; use std::fs; use std::path::{Path, PathBuf}; @@ -10,7 +11,8 @@ pub struct RusticSQLTest { impl RusticSQLTest { pub fn new() -> Self { - let temp_dir = Path::new("./tests/temp_tables"); + let path = "./tests/temp_tables_".to_string() + &get_temp_id().to_string(); + let temp_dir = Path::new(&path); if temp_dir.exists() { fs::remove_dir_all(temp_dir).expect("failed to clean previous test tables."); } @@ -31,6 +33,10 @@ impl RusticSQLTest { } } + pub fn tear_down(&self) { + fs::remove_dir_all(&self.temp_dir).expect("failed to clean up test directory"); + } + pub fn args_for(&self, query: String) -> Vec { vec![ "target/debug/rustic-sql".to_string(), @@ -43,19 +49,23 @@ impl RusticSQLTest { run(self.args_for(query)) } - pub fn run_with_output(&self, query: String) -> String { + pub fn run_and_get_rows(&self, query: String) -> Vec { let args = self.args_for(query); - let output =Command::new(&args[0]) + let output = Command::new(&args[0]) .arg(&args[1]) .arg(&args[2]) .output() .unwrap(); - String::from_utf8(output.stdout).unwrap() + let raw = String::from_utf8(output.stdout).unwrap(); + raw.trim() + .split("\n") + .map(|s| s.to_string()) + .collect::>() } } impl Drop for RusticSQLTest { fn drop(&mut self) { - fs::remove_dir_all(&self.temp_dir).expect("Failed to clean up test directory"); + self.tear_down() } } From 08cf01903251cbc5d0bf33488c06767b2e28ce45 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 04:23:45 -0300 Subject: [PATCH 069/103] added select integration tests and fixed some bugs --- src/query/executor/insert.rs | 2 +- src/query/executor/select.rs | 33 +++++++++++++-- src/query/executor/update.rs | 2 +- src/query/structs/row.rs | 26 ++++++++---- tests/integration.rs | 19 --------- tests/select.rs | 82 ++++++++++++++++++++++++++++++++++++ 6 files changed, 131 insertions(+), 33 deletions(-) delete mode 100644 tests/integration.rs create mode 100644 tests/select.rs diff --git a/src/query/executor/insert.rs b/src/query/executor/insert.rs index 16b0266..5cf7396 100644 --- a/src/query/executor/insert.rs +++ b/src/query/executor/insert.rs @@ -14,7 +14,7 @@ impl Executor { let mut row = Row::new(&header); row.clear()?; row.insert_values(&self.query.columns, fields)?; - writeln!(table, "{}", row.as_csv_string())? + writeln!(table, "{}", row.as_csv_row())? } Ok(()) } diff --git a/src/query/executor/select.rs b/src/query/executor/select.rs index 2ac0744..cee86b7 100644 --- a/src/query/executor/select.rs +++ b/src/query/executor/select.rs @@ -1,9 +1,11 @@ +use crate::errored; use crate::query::executor::Executor; use crate::query::structs::comparator::ExpressionComparator; use crate::query::structs::expression::ExpressionNode; use crate::query::structs::ordering::OrderKind; use crate::query::structs::row::Row; use crate::utils::errors::Errored; +use crate::utils::errors::Errored::Column; use crate::utils::files::{extract_header, get_table_file, split_csv}; use std::cmp::Ordering; use std::io::{BufRead, BufReader}; @@ -13,7 +15,7 @@ impl Executor { let table = get_table_file(&self.table_path)?; let mut reader = BufReader::new(&table); let header = extract_header(&mut reader)?; - println!("{}", header.join(",")); + self.validate_projection(&header)?; let mut matched_rows: Vec = vec![]; for line in reader.lines() { let l = line?; @@ -24,13 +26,21 @@ impl Executor { matched_rows.push(row) } } - self.sort_rows(&mut matched_rows); + self.sort_rows(&mut matched_rows, &header)?; + println!("{}", header.join(",")); self.output_rows(&matched_rows); Ok(()) } - fn sort_rows(&mut self, matched_rows: &mut [Row]) { + fn sort_rows(&mut self, matched_rows: &mut [Row], header: &[String]) -> Result<(), Errored> { for order in &self.query.ordering { + if !header.contains(&order.field.value) { + errored!( + Column, + "order by failed, column {} does not exist", + &order.field.value + ) + } matched_rows.sort_by(|a, b| { let l = ExpressionNode::get_variable_value(&a.values, &order.field); let r = ExpressionNode::get_variable_value(&b.values, &order.field); @@ -45,11 +55,26 @@ impl Executor { Ordering::Equal }) } + Ok(()) } fn output_rows(&self, matched_rows: &[Row]) { for row in matched_rows { - row.print_values() + row.print_projection(&self.query.columns) } } + + fn validate_projection(&self, header: &[String]) -> Result<(), Errored> { + for column in &self.query.columns { + let value = &column.value; + if !header.contains(value) { + errored!( + Column, + "column {} in projection does not exist in table.", + value + ) + } + } + Ok(()) + } } diff --git a/src/query/executor/update.rs b/src/query/executor/update.rs index 08046cb..7ff170d 100644 --- a/src/query/executor/update.rs +++ b/src/query/executor/update.rs @@ -21,7 +21,7 @@ impl Executor { row.read_new_row(fields)?; if row.matches_condition(&self.query)? { row.apply_updates(&self.query.updates)?; - writeln!(writer, "{}", row.as_csv_string())? + writeln!(writer, "{}", row.as_csv_row())? } else { writeln!(writer, "{}", l)? } diff --git a/src/query/structs/row.rs b/src/query/structs/row.rs index 0ccf698..4119a33 100644 --- a/src/query/structs/row.rs +++ b/src/query/structs/row.rs @@ -75,17 +75,26 @@ impl<'a> Row<'a> { Ok(()) } - pub fn as_csv_string(&self) -> String { - let mut fields: Vec<&str> = Vec::new(); - for key in self.header { + fn as_csv_projection(&self, fields: &Vec) -> String { + let mut projection: Vec<&str> = Vec::new(); + for key in fields { let value = self.values.get(key).map(|v| v.as_str()).unwrap_or(""); - fields.push(value); + projection.push(value); } - fields.join(",") + projection.join(",") + } + + pub fn as_csv_row(&self) -> String { + self.as_csv_projection(self.header) } - pub fn print_values(&self) { - println!("{}", self.as_csv_string()); + pub fn print_projection(&self, columns: &[Token]) { + if columns.is_empty() { + println!("{}", self.as_csv_row()); + } else { + let values: Vec = columns.iter().map(|t| t.value.to_string()).collect(); + println!("{}", self.as_csv_projection(&values)); + } } pub fn matches_condition(&self, query: &Query) -> Result { @@ -102,6 +111,7 @@ mod tests { use crate::query::structs::expression::{ExpressionNode, ExpressionOperator}; use crate::query::structs::token::Token; use crate::query::structs::token::TokenKind::*; + use std::default::Default; #[test] fn test_initializing() { @@ -206,7 +216,7 @@ mod tests { row.set("id", "360".to_string()).unwrap(); row.set("apellido", "katta".to_string()).unwrap(); - let csv_string = row.as_csv_string(); + let csv_string = row.as_csv_row(); assert_eq!(csv_string, "360,katta"); } diff --git a/tests/integration.rs b/tests/integration.rs deleted file mode 100644 index 7954186..0000000 --- a/tests/integration.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::utils::RusticSQLTest; - -mod utils; - -#[test] -fn test_empty_query() { - let test = RusticSQLTest::new(); - let result = test.run_for("".to_string()); - assert!(result.is_err_and(|e| e.to_string().contains("empty"))); -} - -#[test] -fn test_select_no_where() { - let test = RusticSQLTest::new(); - let query = "SELECT * FROM users"; - let expected_rows = 11; //with header - let result = test.run_and_get_rows(query.to_string()); - assert_eq!(expected_rows, result.len()); -} diff --git a/tests/select.rs b/tests/select.rs new file mode 100644 index 0000000..38f10dd --- /dev/null +++ b/tests/select.rs @@ -0,0 +1,82 @@ +use crate::utils::RusticSQLTest; + +mod utils; + +#[test] +fn test_empty_query() { + let test = RusticSQLTest::new(); + let result = test.run_for("".to_string()); + assert!(result.is_err_and(|e| e.to_string().contains("empty"))); +} + +#[test] +fn test_select_no_where() { + let test = RusticSQLTest::new(); + let query = "SELECT * FROM users"; + let expected_rows = 11; //with header + let result = test.run_and_get_rows(query.to_string()); + assert_eq!(expected_rows, result.len()); +} + +#[test] +fn test_select_where_with_order_by() { + let test = RusticSQLTest::new(); + let query = "SELECT name, email FROM users WHERE age > 30 ORDER BY age DESC"; + let expected_rows: Vec = [ + vec!["Bob Brown", "bob.brown@example.com"], + vec!["Frank Miller", "frank.miller@example.com"], + vec!["Henry Clark", "henry.clark@example.com"], + vec!["Jane Smith", "jane.smith@example.com"], + vec!["Eve Adams", "eve.adams@example.com"], + vec!["Charlie Davis", "charlie.davis@example.com"], + ] + .iter() + .map(|r| r.join(",")) + .collect(); + let result = test.run_and_get_rows(query.to_string()); + assert_eq!(expected_rows, result[1..]); +} + +#[test] +fn test_select_with_nested_where() { + let test = RusticSQLTest::new(); + let query = "SELECT user_id, name FROM users WHERE age > 30 AND (user_id < 8 OR name = 'Henry Clark') ORDER BY name"; + + let expected_rows: Vec = [ + vec!["4", "Bob Brown"], + vec!["5", "Charlie Davis"], + vec!["7", "Eve Adams"], + vec!["10", "Henry Clark"], + vec!["2", "Jane Smith"], + ] + .iter() + .map(|r| r.join(",")) + .collect(); + + let result = test.run_and_get_rows(query.to_string()); + assert_eq!(expected_rows, result[1..]); +} + +#[test] +fn test_select_with_invalid_order_field() { + let test = RusticSQLTest::new(); + let query = "SELECT user_id, name FROM users ORDER BY psn_id"; + let result = test.run_for(query.to_string()); + assert!(result.is_err_and(|x| x.to_string().contains("exist"))) +} + +#[test] +fn test_select_with_where_with_invalid_field() { + let test = RusticSQLTest::new(); + let query = "SELECT user_id, name FROM users WHERE psn_id = 5"; + let result = test.run_for(query.to_string()); + assert!(result.is_err_and(|x| x.to_string().contains("exist"))) +} + +#[test] +fn test_select_with_invalid_table() { + let test = RusticSQLTest::new(); + let query = "SELECT user_id, name FROM users2 WHERE user_id = 5"; + let result = test.run_for(query.to_string()); + assert!(result.is_err_and(|x| x.to_string().contains("exist"))) +} From f62ac466fab577cb0b9a42d2563ed6b54d07fc2f Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 05:14:17 -0300 Subject: [PATCH 070/103] added insert integration tests and fixed some bugs --- src/query/executor/insert.rs | 2 ++ src/query/structs/row.rs | 2 +- src/utils/files.rs | 15 ++++++++ tests/insert.rs | 70 ++++++++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 tests/insert.rs diff --git a/src/query/executor/insert.rs b/src/query/executor/insert.rs index 5cf7396..4f5561c 100644 --- a/src/query/executor/insert.rs +++ b/src/query/executor/insert.rs @@ -1,6 +1,7 @@ use crate::query::executor::Executor; use crate::query::structs::row::Row; use crate::utils::errors::Errored; +use crate::utils::files; use crate::utils::files::{extract_header, get_table_file}; use std::io::{BufReader, Write}; @@ -9,6 +10,7 @@ impl Executor { let mut table = get_table_file(&self.table_path)?; let mut reader = BufReader::new(&table); let header = extract_header(&mut reader)?; + files::make_file_end_in_newline(&mut table)?; for insert in &self.query.inserts { let fields: Vec = insert.iter().map(|t| t.value.to_string()).collect(); let mut row = Row::new(&header); diff --git a/src/query/structs/row.rs b/src/query/structs/row.rs index 4119a33..2a021ea 100644 --- a/src/query/structs/row.rs +++ b/src/query/structs/row.rs @@ -57,7 +57,7 @@ impl<'a> Row<'a> { if self.header.len() != values.len() { errored!( Table, - "new row ({}) has less fields than table needs ({}).", + "new row has ({}) fields but table needs ({}).", values.len(), self.header.len() ); diff --git a/src/utils/files.rs b/src/utils/files.rs index 296df3d..2106612 100644 --- a/src/utils/files.rs +++ b/src/utils/files.rs @@ -4,6 +4,7 @@ use crate::utils::errors::Errored::Default; use std::fs::File; use std::hash::{DefaultHasher, Hash, Hasher}; use std::io::{BufRead, BufReader}; +use std::io::{Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; use std::{fs, thread}; @@ -69,6 +70,20 @@ pub fn delete_temp_file(table_path: &Path, temp_path: &Path) -> Result<(), Error Ok(()) } +pub fn make_file_end_in_newline(file: &mut File) -> Result<(), Errored> { + file.seek(SeekFrom::End(0))?; + if file.metadata()?.len() == 0 { + return Ok(()); + } + let mut last_byte = [0; 1]; + file.seek(SeekFrom::End(-1))?; + file.read_exact(&mut last_byte)?; + if last_byte[0] != b'\n' { + file.write_all(&[b'\n'])?; + } + Ok(()) +} + pub fn get_table_file(table_path: &Path) -> Result { Ok(File::options().read(true).append(true).open(table_path)?) } diff --git a/tests/insert.rs b/tests/insert.rs new file mode 100644 index 0000000..b3ffe69 --- /dev/null +++ b/tests/insert.rs @@ -0,0 +1,70 @@ +use crate::utils::RusticSQLTest; + +mod utils; + +fn verify_insert(test: &RusticSQLTest, query: &str, expected_row: &[&str]) { + let expected_row_str = expected_row.join(","); + let select_result = test.run_and_get_rows(query.to_string()); + assert_eq!(select_result[1], expected_row_str); +} + +#[test] +fn test_insert_user_all_fields() { + let test = RusticSQLTest::new(); + let query = "INSERT INTO users (user_id, name, email, age) VALUES (14, 'Solidus Snake', 'solidus.snake@mgs.com', 40)"; + let result = test.run_for(query.to_string()); + assert!(result.is_ok()); + let select_query = "SELECT * FROM users WHERE user_id = 14"; + verify_insert( + &test, + select_query, + &["14", "Solidus Snake", "solidus.snake@mgs.com", "40"], + ) +} + +#[test] +fn test_insert_multiple_rows() { + let test = RusticSQLTest::new(); + let insert_query = "INSERT INTO users (user_id, name, email, age) VALUES (15, 'Raiden', 'raiden@mgs.com', 33), (16, 'Big Boss', 'big.boss@mgs.com', 45)"; + let result = test.run_for(insert_query.to_string()); + assert!(result.is_ok()); + let select_query = "SELECT * FROM users WHERE user_id = 15"; + verify_insert( + &test, + select_query, + &["15", "Raiden", "raiden@mgs.com", "33"], + ); + let select_query = "SELECT * FROM users WHERE user_id = 16"; + verify_insert( + &test, + select_query, + &["16", "Big Boss", "big.boss@mgs.com", "45"], + ); +} + +#[test] +fn test_insert_missing_user_id_and_name() { + let test = RusticSQLTest::new(); + let insert_query = "INSERT INTO users (email, age) VALUES ('liquid.snake@mgs.com', 35)"; + let result = test.run_for(insert_query.to_string()); + assert!(result.is_ok()); + let select_query = "SELECT * FROM users WHERE email = 'liquid.snake@mgs.com'"; + verify_insert(&test, select_query, &["", "", "liquid.snake@mgs.com", "35"]); +} + +#[test] +fn test_insert_invalid_column() { + let test = RusticSQLTest::new(); + let insert_query = "INSERT INTO users (user_id, name, email, age, extra_column) VALUES (20, 'Raiden', 'raiden@mgs.com', 33, 'extra')"; + let result = test.run_for(insert_query.to_string()); + assert!(result.is_err_and(|e| e.to_string().contains("exist"))); +} + +#[test] +fn test_insert_invalid_table() { + let test = RusticSQLTest::new(); + let insert_query = + "INSERT INTO kojima (user_id, name, email, age) VALUES (21, 'Quiet', 'quiet@mgs.com', 27)"; + let result = test.run_for(insert_query.to_string()); + assert!(result.is_err_and(|e| e.to_string().contains("table"))); +} From d48c13bdaba5779b355eb464aec5d9efa411249a Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 05:56:49 -0300 Subject: [PATCH 071/103] added update integration tests --- src/query/structs/query.rs | 18 ++++++++ tests/insert.rs | 35 +++++---------- tests/integration_tables/orders.csv | 11 ----- tests/integration_tables/pokemon.csv | 11 +++++ tests/select.rs | 16 +++---- tests/update.rs | 50 +++++++++++++++++++++ tests/utils/mod.rs | 67 ++++++++++++++++++++-------- 7 files changed, 146 insertions(+), 62 deletions(-) delete mode 100644 tests/integration_tables/orders.csv create mode 100644 tests/integration_tables/pokemon.csv create mode 100644 tests/update.rs diff --git a/src/query/structs/query.rs b/src/query/structs/query.rs index 5ffd6ba..4d3cd4e 100644 --- a/src/query/structs/query.rs +++ b/src/query/structs/query.rs @@ -82,3 +82,21 @@ impl Debug for Query { write!(f, "{}", &self) } } + +#[cfg(test)] +mod test { + use crate::query::structs::query::Query; + use crate::query::structs::token::Token; + use crate::utils::errors::Errored; + + #[test] + fn test_invalid_query() { + let tokens = vec![Token::default()]; + let result = Query::from(tokens); + assert!(result.is_err(), "should be errored with unknown token."); + match result { + Err(Errored::Syntax(msg)) => assert!(msg.contains("valid operation")), + _ => panic!("expected syntax error for query starting keyword."), + } + } +} diff --git a/tests/insert.rs b/tests/insert.rs index b3ffe69..2e7b3e0 100644 --- a/tests/insert.rs +++ b/tests/insert.rs @@ -1,22 +1,15 @@ use crate::utils::RusticSQLTest; -mod utils; - -fn verify_insert(test: &RusticSQLTest, query: &str, expected_row: &[&str]) { - let expected_row_str = expected_row.join(","); - let select_result = test.run_and_get_rows(query.to_string()); - assert_eq!(select_result[1], expected_row_str); -} +pub mod utils; #[test] fn test_insert_user_all_fields() { - let test = RusticSQLTest::new(); + let test = RusticSQLTest::default(); let query = "INSERT INTO users (user_id, name, email, age) VALUES (14, 'Solidus Snake', 'solidus.snake@mgs.com', 40)"; let result = test.run_for(query.to_string()); assert!(result.is_ok()); let select_query = "SELECT * FROM users WHERE user_id = 14"; - verify_insert( - &test, + test.assert_row( select_query, &["14", "Solidus Snake", "solidus.snake@mgs.com", "40"], ) @@ -24,37 +17,29 @@ fn test_insert_user_all_fields() { #[test] fn test_insert_multiple_rows() { - let test = RusticSQLTest::new(); + let test = RusticSQLTest::default(); let insert_query = "INSERT INTO users (user_id, name, email, age) VALUES (15, 'Raiden', 'raiden@mgs.com', 33), (16, 'Big Boss', 'big.boss@mgs.com', 45)"; let result = test.run_for(insert_query.to_string()); assert!(result.is_ok()); let select_query = "SELECT * FROM users WHERE user_id = 15"; - verify_insert( - &test, - select_query, - &["15", "Raiden", "raiden@mgs.com", "33"], - ); + test.assert_row(select_query, &["15", "Raiden", "raiden@mgs.com", "33"]); let select_query = "SELECT * FROM users WHERE user_id = 16"; - verify_insert( - &test, - select_query, - &["16", "Big Boss", "big.boss@mgs.com", "45"], - ); + test.assert_row(select_query, &["16", "Big Boss", "big.boss@mgs.com", "45"]); } #[test] fn test_insert_missing_user_id_and_name() { - let test = RusticSQLTest::new(); + let test = RusticSQLTest::default(); let insert_query = "INSERT INTO users (email, age) VALUES ('liquid.snake@mgs.com', 35)"; let result = test.run_for(insert_query.to_string()); assert!(result.is_ok()); let select_query = "SELECT * FROM users WHERE email = 'liquid.snake@mgs.com'"; - verify_insert(&test, select_query, &["", "", "liquid.snake@mgs.com", "35"]); + test.assert_row(select_query, &["", "", "liquid.snake@mgs.com", "35"]); } #[test] fn test_insert_invalid_column() { - let test = RusticSQLTest::new(); + let test = RusticSQLTest::default(); let insert_query = "INSERT INTO users (user_id, name, email, age, extra_column) VALUES (20, 'Raiden', 'raiden@mgs.com', 33, 'extra')"; let result = test.run_for(insert_query.to_string()); assert!(result.is_err_and(|e| e.to_string().contains("exist"))); @@ -62,7 +47,7 @@ fn test_insert_invalid_column() { #[test] fn test_insert_invalid_table() { - let test = RusticSQLTest::new(); + let test = RusticSQLTest::default(); let insert_query = "INSERT INTO kojima (user_id, name, email, age) VALUES (21, 'Quiet', 'quiet@mgs.com', 27)"; let result = test.run_for(insert_query.to_string()); diff --git a/tests/integration_tables/orders.csv b/tests/integration_tables/orders.csv deleted file mode 100644 index 0c5ac02..0000000 --- a/tests/integration_tables/orders.csv +++ /dev/null @@ -1,11 +0,0 @@ -order_id,customer_name,product_name,quantity,price_per_unit -1001,John Doe,Laptop,1,999 -1002,Jane Smith,Smartphone,2,499 -1003,Alice Johnson,Headphones,3,149 -1004,Bob Brown,Keyboard,1,89 -1005,Charlie Davis,Mouse,2,29 -1006,David Wilson,Monitor,1,279 -1007,Eve Adams,Desk Chair,1,159 -1008,Frank Miller,Webcam,1,89 -1009,Grace Lee,USB Drive,5,15 -1010,Henry Clark,External Hard Drive,1,119 \ No newline at end of file diff --git a/tests/integration_tables/pokemon.csv b/tests/integration_tables/pokemon.csv new file mode 100644 index 0000000..a9f1435 --- /dev/null +++ b/tests/integration_tables/pokemon.csv @@ -0,0 +1,11 @@ +id,name,type,level +1,Pikachu,Electric,25 +2,Charmander,Fire,18 +3,Bulbasaur,Grass,15 +4,Squirtle,Water,16 +5,Jigglypuff,Fairy,22 +6,Meowth,Normal,20 +7,Psyduck,Water,19 +8,Machop,Fighting,23 +9,Geodude,Rock,17 +10,Onix,Rock,30 diff --git a/tests/select.rs b/tests/select.rs index 38f10dd..559cf17 100644 --- a/tests/select.rs +++ b/tests/select.rs @@ -1,17 +1,17 @@ use crate::utils::RusticSQLTest; -mod utils; +pub mod utils; #[test] fn test_empty_query() { - let test = RusticSQLTest::new(); + let test = RusticSQLTest::default(); let result = test.run_for("".to_string()); assert!(result.is_err_and(|e| e.to_string().contains("empty"))); } #[test] fn test_select_no_where() { - let test = RusticSQLTest::new(); + let test = RusticSQLTest::default(); let query = "SELECT * FROM users"; let expected_rows = 11; //with header let result = test.run_and_get_rows(query.to_string()); @@ -20,7 +20,7 @@ fn test_select_no_where() { #[test] fn test_select_where_with_order_by() { - let test = RusticSQLTest::new(); + let test = RusticSQLTest::default(); let query = "SELECT name, email FROM users WHERE age > 30 ORDER BY age DESC"; let expected_rows: Vec = [ vec!["Bob Brown", "bob.brown@example.com"], @@ -39,7 +39,7 @@ fn test_select_where_with_order_by() { #[test] fn test_select_with_nested_where() { - let test = RusticSQLTest::new(); + let test = RusticSQLTest::default(); let query = "SELECT user_id, name FROM users WHERE age > 30 AND (user_id < 8 OR name = 'Henry Clark') ORDER BY name"; let expected_rows: Vec = [ @@ -59,7 +59,7 @@ fn test_select_with_nested_where() { #[test] fn test_select_with_invalid_order_field() { - let test = RusticSQLTest::new(); + let test = RusticSQLTest::default(); let query = "SELECT user_id, name FROM users ORDER BY psn_id"; let result = test.run_for(query.to_string()); assert!(result.is_err_and(|x| x.to_string().contains("exist"))) @@ -67,7 +67,7 @@ fn test_select_with_invalid_order_field() { #[test] fn test_select_with_where_with_invalid_field() { - let test = RusticSQLTest::new(); + let test = RusticSQLTest::default(); let query = "SELECT user_id, name FROM users WHERE psn_id = 5"; let result = test.run_for(query.to_string()); assert!(result.is_err_and(|x| x.to_string().contains("exist"))) @@ -75,7 +75,7 @@ fn test_select_with_where_with_invalid_field() { #[test] fn test_select_with_invalid_table() { - let test = RusticSQLTest::new(); + let test = RusticSQLTest::default(); let query = "SELECT user_id, name FROM users2 WHERE user_id = 5"; let result = test.run_for(query.to_string()); assert!(result.is_err_and(|x| x.to_string().contains("exist"))) diff --git a/tests/update.rs b/tests/update.rs new file mode 100644 index 0000000..434ff4e --- /dev/null +++ b/tests/update.rs @@ -0,0 +1,50 @@ +use crate::utils::RusticSQLTest; + +pub mod utils; + +#[test] +fn test_update_pokemon_with_empty_name() { + let test = RusticSQLTest::default(); + let update_query = "UPDATE pokemon SET name = '' WHERE id = 1"; + let result = test.run_for(update_query.to_string()); + assert!(result.is_ok()); + let select_query = "SELECT * FROM pokemon WHERE id = 1"; + let expected_row = ["1", "", "Electric", "25"]; + test.assert_row(select_query, &expected_row); +} + +#[test] +fn test_update_all_pokemon_to_same_level() { + let test = RusticSQLTest::default(); + let update_query = "UPDATE pokemon SET level = 50, name = '', type = ''"; + let result = test.run_for(update_query.to_string()); + assert!(result.is_ok()); + for id in 1..=10 { + let select_query = format!("SELECT * FROM pokemon WHERE id = {}", id); + let expected_row = [&id.to_string(), "", "", "50"]; + test.assert_row(&select_query, &expected_row); + } +} + +#[test] +fn test_update_non_existent_type() { + let test = RusticSQLTest::default(); + let update_query = "UPDATE pokemon SET level = 50 WHERE type = 'Mythical'"; + test.verify_no_changes("pokemon.csv".to_string(), update_query); +} + +#[test] +fn test_missing_set_in_query() { + let test = RusticSQLTest::default(); + let update_query = "UPDATE pokemon level = 50'"; + let result = test.run_for(update_query.to_string()); + assert!(result.is_err()); +} + +#[test] +fn test_update_invalid_column() { + let test = RusticSQLTest::default(); + let update_query = "UPDATE pokemon SET hp = 80 WHERE type = 'Fire'"; + let result = test.run_for(update_query.to_string()); + assert!(result.is_err_and(|e| e.to_string().contains("hp"))); +} diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs index f8c9fda..47d67b1 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -2,6 +2,8 @@ use rustic_sql::run; use rustic_sql::utils::files::get_temp_id; use std::error::Error; use std::fs; +use std::fs::File; +use std::io::{BufRead, BufReader}; use std::path::{Path, PathBuf}; use std::process::Command; @@ -10,27 +12,31 @@ pub struct RusticSQLTest { } impl RusticSQLTest { - pub fn new() -> Self { - let path = "./tests/temp_tables_".to_string() + &get_temp_id().to_string(); - let temp_dir = Path::new(&path); - if temp_dir.exists() { - fs::remove_dir_all(temp_dir).expect("failed to clean previous test tables."); - } - fs::create_dir(temp_dir).expect("failed to create temp table directory."); - - let og_tables_path = Path::new("./tests/integration_tables"); - let orders = og_tables_path.join("orders.csv"); - let users = og_tables_path.join("users.csv"); - - let temp_orders = temp_dir.join("orders.csv"); - let temp_users = temp_dir.join("users.csv"); + pub fn assert_row(&self, query: &str, expected_row: &[&str]) { + let expected_row_str = expected_row.join(","); + let select_result = self.run_and_get_rows(query.to_string()); + assert_eq!(select_result[1], expected_row_str); + } - fs::copy(orders, &temp_orders).expect("failed to copy order table."); - fs::copy(users, &temp_users).expect("failed to copy user table."); + fn read_table_to_string(&self, table: &String) -> String { + let file = File::open(self.temp_dir.join(table)).unwrap(); + let reader = BufReader::new(file); + let mut content = String::new(); + for line in reader.lines() { + content.push_str(&line.unwrap()); + content.push('\n'); + } + content + } - RusticSQLTest { - temp_dir: temp_dir.to_path_buf(), + pub fn verify_no_changes(&self, table: String, query: &str) { + let before_query = self.read_table_to_string(&table); + let result = self.run_for(query.to_string()); + if result.is_err() { + panic!("error executing query"); } + let after_query = self.read_table_to_string(&table); + assert_eq!(before_query, after_query) } pub fn tear_down(&self) { @@ -64,6 +70,31 @@ impl RusticSQLTest { } } +impl Default for RusticSQLTest { + fn default() -> Self { + let path = "./tests/temp_tables_".to_string() + &get_temp_id().to_string(); + let temp_dir = Path::new(&path); + if temp_dir.exists() { + fs::remove_dir_all(temp_dir).expect("failed to clean previous test tables."); + } + fs::create_dir(temp_dir).expect("failed to create temp table directory."); + + let og_tables_path = Path::new("./tests/integration_tables"); + let pokemons = og_tables_path.join("pokemon.csv"); + let users = og_tables_path.join("users.csv"); + + let temp_orders = temp_dir.join("pokemon.csv"); + let temp_users = temp_dir.join("users.csv"); + + fs::copy(pokemons, &temp_orders).expect("failed to copy order table."); + fs::copy(users, &temp_users).expect("failed to copy user table."); + + RusticSQLTest { + temp_dir: temp_dir.to_path_buf(), + } + } +} + impl Drop for RusticSQLTest { fn drop(&mut self) { self.tear_down() From 76f325f5f53619b2fec9022443f624b3bc09b4b5 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 06:07:58 -0300 Subject: [PATCH 072/103] added delete integration tests --- tests/delete.rs | 40 ++++++++++++++++++++++++++++++++++++++++ tests/utils/mod.rs | 6 +++++- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 tests/delete.rs diff --git a/tests/delete.rs b/tests/delete.rs new file mode 100644 index 0000000..434c908 --- /dev/null +++ b/tests/delete.rs @@ -0,0 +1,40 @@ +use crate::utils::RusticSQLTest; + +pub mod utils; + +#[test] +fn test_delete_single_row() { + let test = RusticSQLTest::default(); + let delete_query = "DELETE FROM pokemon WHERE id = 1"; + let result = test.run_for(delete_query.to_string()); + assert!(result.is_ok()); + let select_query = "SELECT * FROM pokemon WHERE id = 1"; + test.assert_row(select_query, &[]); +} + +#[test] +fn test_delete_multiple_rows() { + let test = RusticSQLTest::default(); + let delete_query = "DELETE FROM pokemon WHERE type != 'Electric'"; + let result = test.run_for(delete_query.to_string()); + assert!(result.is_ok()); + let select_query = "SELECT * FROM pokemon WHERE type != 'Electric'"; + test.assert_row(select_query, &[]); +} + +#[test] +fn test_delete_all() { + let test = RusticSQLTest::default(); + let delete_query = "DELETE FROM pokemon"; + let result = test.run_for(delete_query.to_string()); + assert!(result.is_ok()); + let select_query = "SELECT * FROM pokemon'"; + test.assert_row(select_query, &[]); +} + +#[test] +fn test_delete_none() { + let test = RusticSQLTest::default(); + let delete_query = "DELETE FROM pokemon WHERE type = 'Sound'"; + test.verify_no_changes("pokemon.csv".to_string(), delete_query); +} diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs index 47d67b1..110c2d7 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -15,7 +15,11 @@ impl RusticSQLTest { pub fn assert_row(&self, query: &str, expected_row: &[&str]) { let expected_row_str = expected_row.join(","); let select_result = self.run_and_get_rows(query.to_string()); - assert_eq!(select_result[1], expected_row_str); + if select_result.len() == 1 { + assert_eq!("", expected_row_str); + } else { + assert_eq!(select_result[1], expected_row_str); + } } fn read_table_to_string(&self, table: &String) -> String { From fbac191f33c0720f18cb9bb27976fc2fbc21fea4 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 08:43:31 -0300 Subject: [PATCH 073/103] added documentation --- src/lib.rs | 16 +++ src/query/builder/delete.rs | 47 +++++++ src/query/builder/expression.rs | 137 ++++++++++++++++++++ src/query/builder/insert.rs | 53 ++++++++ src/query/builder/mod.rs | 123 ++++++++++++++++++ src/query/builder/select.rs | 44 +++++++ src/query/builder/update.rs | 45 +++++++ src/query/executor/delete.rs | 16 +++ src/query/executor/insert.rs | 15 +++ src/query/executor/mod.rs | 50 ++++++++ src/query/executor/select.rs | 49 +++++++ src/query/executor/update.rs | 16 +++ src/query/structs/comparator.rs | 41 ++++++ src/query/structs/expression.rs | 57 ++++++++ src/query/structs/operation.rs | 9 ++ src/query/structs/ordering.rs | 17 +++ src/query/structs/query.rs | 51 ++++++-- src/query/structs/row.rs | 221 +++++++++++++++++++++++++++++++- src/query/structs/token.rs | 41 ++++++ src/query/tokenizer.rs | 127 ++++++++++++++++++ src/utils/errors.rs | 34 ++++- src/utils/files.rs | 112 ++++++++++++++++ 22 files changed, 1307 insertions(+), 14 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6991bc4..fe94fb1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,22 @@ use std::error::Error; pub mod query; pub mod utils; +/// Ejecuta la aplicación RusticSQL a partir de los argumentos proporcionados. +/// +/// Esta función valida los argumentos de la línea de comandos, procesa la consulta SQL y ejecuta +/// la consulta en los datos especificados. +/// +/// # Argumentos +/// +/// - `args`: Un vector de `String` que contiene los argumentos de la línea de comandos. Se espera que +/// contenga exactamente tres elementos: el nombre del comando, la ruta a las tablas y la consulta SQL. +/// +/// # Errores +/// +/// - Retorna un error si el número de argumentos es incorrecto. +/// - Retorna un error si la ruta a las tablas no es válida. +/// - Retorna un error si la consulta SQL no es válida. +/// - Retorna un error si el procesamiento de la consulta o la ejecución falla. pub fn run(args: Vec) -> Result<(), Box> { if args.len() != 3 { println!("invalid usage of rustic-sql"); diff --git a/src/query/builder/delete.rs b/src/query/builder/delete.rs index 49fd1ff..a431409 100644 --- a/src/query/builder/delete.rs +++ b/src/query/builder/delete.rs @@ -8,17 +8,42 @@ use std::collections::VecDeque; const ALLOWED_KEYWORDS: &[&str] = &["FROM", "WHERE", "AND", "OR"]; +/// Constructor para consultas de eliminación (`DELETE`). +/// +/// `DeleteBuilder` se encarga de construir una consulta de eliminación a partir +/// de una lista de tokens que representan la sintaxis de una consulta SQL. pub struct DeleteBuilder { tokens: VecDeque, } impl DeleteBuilder { + /// Crea una nueva instancia de `DeleteBuilder`. + /// + /// Este constructor inicializa el `DeleteBuilder` con una cola de tokens + /// que representan una consulta SQL. + /// + /// # Parámetros + /// + /// - `tokens`: Cola de tokens (`VecDeque`) que se utilizarán para construir la consulta. pub fn new(tokens: VecDeque) -> Self { Self { tokens } } } impl Builder for DeleteBuilder { + /// Construye una consulta `DELETE` a partir de los tokens proporcionados. + /// + /// Este método analiza los tokens para construir una consulta de eliminación válida. + /// Verifica palabras clave permitidas, obtiene la tabla objetivo y, opcionalmente, + /// analiza las condiciones de la cláusula `WHERE`. + /// + /// # Retorno + /// + /// Retorna un objeto `Query` con la operación de eliminación configurada. + /// + /// # Errores + /// + /// Retorna un error `Errored` si la consulta contiene errores de sintaxis o palabras clave no permitidas. fn build(&mut self) -> Result { let mut query = Query::default(); self.validate_keywords()?; @@ -33,10 +58,32 @@ impl Builder for DeleteBuilder { Ok(query) } + /// Devuelve una referencia mutable a los tokens de la consulta. + /// + /// Este método es utilizado por otros métodos de construcción para acceder y modificar + /// la lista de tokens durante el proceso de análisis. + /// + /// # Retorno + /// + /// Retorna una referencia mutable a la cola de tokens (`VecDeque`). fn tokens(&mut self) -> &mut VecDeque { &mut self.tokens } + /// Valida las palabras clave permitidas en una consulta `DELETE`. + /// + /// Este método verifica que solo se utilicen las palabras clave permitidas para una + /// operación de eliminación. Si encuentra alguna palabra clave no permitida, lanza un error. + /// + /// Las palabras clave permitidas son: `FROM`, `WHERE`, `AND`, `OR`. + /// + /// # Retorno + /// + /// Retorna `Ok(())` si todas las palabras clave son válidas. + /// + /// # Errores + /// + /// Retorna un error `Errored` si se encuentra una palabra clave no válida. fn validate_keywords(&self) -> Result<(), Errored> { validate_keywords(ALLOWED_KEYWORDS, &self.tokens, Delete) } diff --git a/src/query/builder/expression.rs b/src/query/builder/expression.rs index c214ee6..d4e2896 100644 --- a/src/query/builder/expression.rs +++ b/src/query/builder/expression.rs @@ -8,13 +8,67 @@ use crate::utils::errors::Errored; use crate::utils::errors::Errored::Syntax; use std::collections::VecDeque; +/// Estructura para analizar y construir expresiones lógicas en una consulta. +/// +/// `ExpressionBuilder` proporciona métodos recursivos para analizar expresiones condicionales +/// como `AND`, `OR`, `NOT`, y comparaciones simples. Estas expresiones son comúnmente +/// utilizadas en las cláusulas `WHERE` de las consultas SQL. pub struct ExpressionBuilder; impl ExpressionBuilder { + /// Inicia el análisis de las expresiones a partir de una lista de tokens. + /// + /// Este método comienza el proceso de parseo llamando a `parse_or` para + /// manejar operaciones lógicas. + /// + /// La razón por la cual este método arranca parseando las operaciones OR, es porque + /// es la que tiene menor precedencia, de esta manera la expresión sera parseada en el siguiente orden de + /// predecencia: + /// + /// - OR -> AND -> NOT -> (Parantesis) + /// + /// Teniendo prioridad los operadores que estan más la derecha. + /// + /// Podemos hacer esto gracias a la naturaleza recursiva de los métodos que siempre buscaran llegar + /// a una hoja desde arriba e ir evaluando en reversa hasta retornar. + /// + /// # Parámetros + /// + /// - `tokens`: Cola de tokens (`VecDeque`) que representan la consulta a analizar. + /// + /// # Retorno + /// + /// Retorna un nodo de expresión (`ExpressionNode`) que representa la expresión completa. + /// + /// # Errores + /// + /// Retorna un error `Errored` si ocurre algún problema durante el análisis. pub fn parse_expressions(tokens: &mut VecDeque) -> Result { ExpressionBuilder::parse_or(tokens) } + /// Analiza las expresiones con el operador `OR`. + /// + /// Este método evalúa si existen múltiples expresiones unidas por el operador `OR` + /// y las agrupa en un nodo de expresión. + /// + /// Primero el método busca mediante el parseo de una operacion AND el valor de la rama + /// izquierda de la expresión actual. + /// + /// Si el tóken actual es un operador "OR", consumimos el token y buscamos el valor + /// de la derecha. + /// + /// # Parámetros + /// + /// - `tokens`: Cola de tokens a analizar. + /// + /// # Retorno + /// + /// Retorna un nodo de expresión con operadores `OR`. + /// + /// # Errores + /// + /// Retorna un error si hay un problema con los tokens. fn parse_or(tokens: &mut VecDeque) -> Result { let mut left = ExpressionBuilder::parse_and(tokens)?; while let Some(t) = tokens.front() { @@ -32,6 +86,22 @@ impl ExpressionBuilder { Ok(left) } + /// Analiza las expresiones con el operador `AND`. + /// + /// Similar a `parse_or`, pero para operaciones con `AND`. + /// Podemos ver como este método le delega a los operadores `NOT` la responsabilidad de evaluar. + /// + /// # Parámetros + /// + /// - `tokens`: Cola de tokens a analizar. + /// + /// # Retorno + /// + /// Retorna un nodo de expresión con operadores `AND`. + /// + /// # Errores + /// + /// Retorna un error si hay un problema con los tokens. fn parse_and(tokens: &mut VecDeque) -> Result { let mut left = ExpressionBuilder::parse_not(tokens)?; while let Some(t) = tokens.front() { @@ -49,6 +119,25 @@ impl ExpressionBuilder { Ok(left) } + /// Analiza las expresiones con el operador `NOT`. + /// + /// Maneja expresiones que comienzan con `NOT`, invirtiendo la lógica de la condición. + /// + /// Es el operador booleano con mayor precedencia, en caso de no estar dentro de una operación + /// NOT, este método se encarga de empezar a estudiar los nodos más simples ya que ninguno ha matcheado hasta + /// ahora. + /// + /// # Parámetros + /// + /// - `tokens`: Cola de tokens a analizar. + /// + /// # Retorno + /// + /// Retorna un nodo de expresión con el operador `NOT`. + /// + /// # Errores + /// + /// Retorna un error si hay un problema con los tokens. fn parse_not(tokens: &mut VecDeque) -> Result { if let Some(t) = tokens.front() { if t.kind == Keyword && t.value == "NOT" { @@ -64,6 +153,21 @@ impl ExpressionBuilder { ExpressionBuilder::parse_comparisons(tokens) } + /// Analiza las comparaciones simples en las expresiones. + /// + /// Este método procesa las comparaciones entre dos valores usando operadores como `=`, `>`, `<`, etc. + /// + /// # Parámetros + /// + /// - `tokens`: Cola de tokens a analizar. + /// + /// # Retorno + /// + /// Retorna un nodo de expresión que representa una comparación. + /// + /// # Errores + /// + /// Retorna un error si los tokens no forman una comparación válida. fn parse_comparisons(tokens: &mut VecDeque) -> Result { let left = ExpressionBuilder::parse_leaf(tokens)?; let operator = ExpressionBuilder::parse_simple_operator(tokens); @@ -78,6 +182,24 @@ impl ExpressionBuilder { }) } + /// Analiza las hojas de una expresión, como identificadores, números o cadenas. + /// + /// Este método maneja elementos básicos que no son operadores lógicos, como los valores literales. + /// + /// En caso de conseguir un parentesis, es indicador de que una nueva expresión se debe evaluar. + /// Es acá cuando la recursividad vuelve a empezar. + /// + /// # Parámetros + /// + /// - `tokens`: Cola de tokens a analizar. + /// + /// # Retorno + /// + /// Retorna un nodo de expresión que representa una hoja. + /// + /// # Errores + /// + /// Retorna un error si no se encuentra una hoja válida. fn parse_leaf(tokens: &mut VecDeque) -> Result { let mut leaf = Empty; while let Some(t) = tokens.front() { @@ -102,6 +224,21 @@ impl ExpressionBuilder { Ok(leaf) } + /// Analiza los operadores simples en las comparaciones. + /// + /// Este método reconoce operadores como `=`, `!=`, `>`, `<`, etc. + /// + /// # Parámetros + /// + /// - `tokens`: Cola de tokens a analizar. + /// + /// # Retorno + /// + /// Retorna el operador de la comparación (`ExpressionOperator`). + /// + /// # Errores + /// + /// Retorna un error si no se encuentra un operador válido. fn parse_simple_operator(tokens: &mut VecDeque) -> Result { let t = tokens .front() diff --git a/src/query/builder/insert.rs b/src/query/builder/insert.rs index e9e5d6e..63f0717 100644 --- a/src/query/builder/insert.rs +++ b/src/query/builder/insert.rs @@ -10,15 +10,34 @@ use std::collections::VecDeque; const ALLOWED_KEYWORDS: &[&str] = &["VALUES"]; +/// Estructura `InsertBuilder` que permite construir una consulta de tipo INSERT. pub struct InsertBuilder { tokens: VecDeque, } impl InsertBuilder { + /// Crea una nueva instancia de `InsertBuilder` con los tokens proporcionados. + /// + /// # Parámetros + /// - `tokens`: Un `VecDeque` que contiene los tokens de la consulta. + /// + /// # Retorna + /// - Una instancia de `InsertBuilder`. pub fn new(tokens: VecDeque) -> Self { Self { tokens } } + /// Analiza los valores de inserción de una consulta SQL INSERT. + /// + /// Este método espera encontrar la palabra clave `VALUES` seguida de un grupo de valores + /// entre paréntesis. Los valores pueden ser cadenas o números. + /// + /// # Retorna + /// - Un `Result` que contiene un vector de vectores de `Token` representando los valores de inserción. + /// + /// # Errores + /// - Retorna un error si no se encuentra la palabra clave `VALUES` o si los valores no están + /// correctamente formateados. fn parse_insert_values(&mut self) -> Result>, Errored> { self.pop_expecting("VALUES", Keyword)?; self.peek_expecting("(", ParenthesisOpen)?; @@ -45,6 +64,17 @@ impl InsertBuilder { Ok(inserts) } + /// Este método asegura que el número de valores en cada inserción coincida con el número + /// de columnas definidas en la consulta. + /// + /// # Parámetros + /// - `query`: Referencia a la consulta `Query` que contiene las columnas y los valores de inserción. + /// + /// # Retorna + /// - `Ok(())` si los valores son válidos. + /// + /// # Errores + /// - Retorna un error si el número de columnas y el número de valores en la inserción no coinciden. fn validate_inserts(&self, query: &Query) -> Result<(), Errored> { for insert in &query.inserts { let columns = query.columns.len(); @@ -63,6 +93,17 @@ impl InsertBuilder { } impl Builder for InsertBuilder { + /// Construye una consulta de tipo INSERT a partir de los tokens. + /// + /// Este método analiza los tokens, identifica las columnas, los valores a insertar y valida + /// la estructura de la consulta. + /// + /// # Retorna + /// - Un `Result` que contiene la consulta `Query` si se construye exitosamente. + /// + /// # Errores + /// - Retorna un error si la consulta no está correctamente formada o contiene palabras clave + /// inválidas. fn build(&mut self) -> Result { let mut query = Query::default(); self.validate_keywords()?; @@ -76,10 +117,22 @@ impl Builder for InsertBuilder { Ok(query) } + /// Retorna una referencia mutable a los tokens que se están procesando. + /// + /// # Retorna + /// - Una referencia mutable a `VecDeque`. fn tokens(&mut self) -> &mut VecDeque { &mut self.tokens } + /// Este método compara las palabras clave en los tokens con las permitidas para asegurarse + /// de que la consulta sea válida. + /// + /// # Retorna + /// - `Ok(())` si las palabras clave son válidas. + /// + /// # Errores + /// - Retorna un error si se detecta una palabra clave inválida en la consulta. fn validate_keywords(&self) -> Result<(), Errored> { validate_keywords(ALLOWED_KEYWORDS, &self.tokens, Insert) } diff --git a/src/query/builder/mod.rs b/src/query/builder/mod.rs index cbc48e8..0973f19 100644 --- a/src/query/builder/mod.rs +++ b/src/query/builder/mod.rs @@ -18,10 +18,43 @@ use crate::utils::errors::Errored; use crate::utils::errors::Errored::*; use std::collections::VecDeque; +/// Interfaz para construir y validar una consulta SQL. +/// +/// Este trait define los métodos necesarios para analizar y construir consultas SQL, +/// incluyendo la validación de las palabras clave y la extracción de tokens relacionados +/// con tablas, columnas y condiciones. pub trait Builder { + /// Construye y retorna una consulta SQL (`Query`) basada en los tokens disponibles. + /// + /// # Errores + /// + /// Retorna un error de tipo `Errored` si hay problemas al construir la consulta. fn build(&mut self) -> Result; + + /// Retorna una referencia mutable a la cola de tokens (`VecDeque`). + /// + /// # Retorno + /// + /// Una referencia mutable a los tokens procesados por el builder. fn tokens(&mut self) -> &mut VecDeque; + /// Analiza el nombre de la tabla de una consulta SQL. + /// + /// Este método maneja las palabras clave `FROM` para consultas `SELECT` y `DELETE`. + /// En el caso de otras Operaciones SQL, no hay ningun tipo de token que se interponga entre + /// las palabras claves y la tabla, por esto, se saltan. + /// + /// # Parámetros + /// + /// - `operation`: Operación SQL (`Select`, `Delete`, etc.) que define cómo se analiza la tabla. + /// + /// # Retorno + /// + /// Retorna el nombre de la tabla como `String`. + /// + /// # Errores + /// + /// Retorna un error `Errored` si no se encuentra un identificador de tabla válido. fn parse_table(&mut self, operation: Operation) -> Result { match operation { Select | Delete => { @@ -40,6 +73,17 @@ pub trait Builder { Ok(t.value) } + /// Analiza las columnas de una consulta SQL. + /// + /// Este método procesa los tokens que representan columnas en las cláusulas `SELECT` o `INSERT`. + /// + /// # Retorno + /// + /// Retorna un vector de tokens que representan las columnas de la consulta. + /// + /// # Errores + /// + /// Retorna un error `Errored` si se encuentra un token inesperado durante el análisis de columnas. fn parse_columns(&mut self) -> Result, Errored> { let mut fields: Vec = vec![]; while let Some(t) = self.tokens().front() { @@ -69,11 +113,27 @@ pub trait Builder { Ok(fields) } + /// Analiza la cláusula `WHERE` en una consulta SQL. + /// + /// Este método busca la palabra clave `WHERE` y luego construye la expresión correspondiente. + /// + /// # Retorno + /// + /// Retorna un nodo de expresión (`ExpressionNode`) que representa la condición `WHERE`. + /// + /// # Errores + /// + /// Retorna un error `Errored` si no se encuentra o se procesa incorrectamente la cláusula `WHERE`. fn parse_where(&mut self) -> Result { self.pop_expecting("WHERE", Keyword)?; ExpressionBuilder::parse_expressions(self.tokens()) } + /// Valida que no haya más tokens después de la consulta. + /// + /// # Errores + /// + /// Retorna un error `Errored` si se encuentran tokens adicionales al final de la consulta. fn expect_none(&mut self) -> Result<(), Errored> { if let Some(t) = self.tokens().front() { errored!(Syntax, "expected end of query but got: {:?}", t); @@ -81,11 +141,35 @@ pub trait Builder { Ok(()) } + /// Extrae el siguiente token si coincide con el valor y tipo esperados. + /// + /// # Parámetros + /// + /// - `value`: El valor esperado del token. + /// - `kind`: El tipo esperado del token (`TokenKind`). + /// + /// # Retorno + /// + /// Retorna el token extraído si cumple con las expectativas, o `None` si no. + /// + /// # Errores + /// + /// Retorna un error `Errored` si no se encuentra el token esperado. fn pop_expecting(&mut self, value: &str, kind: TokenKind) -> Result, Errored> { self.peek_expecting(value, kind)?; Ok(self.tokens().pop_front()) } + /// Verifica si el siguiente token en la lista coincide con el valor y tipo esperados. + /// + /// # Parámetros + /// + /// - `value`: El valor esperado del token. + /// - `kind`: El tipo esperado del token (`TokenKind`). + /// + /// # Errores + /// + /// Retorna un error `Errored` si no se encuentra el token esperado. fn peek_expecting(&mut self, value: &str, kind: TokenKind) -> Result<(), Errored> { let expected = Token { value: value.to_string(), @@ -101,9 +185,23 @@ pub trait Builder { Ok(()) } + /// Valida que solo se usen palabras clave permitidas en la consulta. + /// + /// # Errores + /// + /// Retorna un error `Errored` si se encuentra una palabra clave inválida. fn validate_keywords(&self) -> Result<(), Errored>; } +/// Determina el tipo de operación SQL a partir de un token. +/// +/// # Parámetros +/// +/// - `token`: Un token opcional que representa una palabra clave de operación SQL. +/// +/// # Retorno +/// +/// Retorna el tipo de operación (`Operation`), como `Select`, `Insert`, `Update`, `Delete`, o `Unknown` si no se reconoce la palabra clave. pub fn get_kind(token: Option) -> Operation { match token { Some(t) => match t.value.as_str() { @@ -117,6 +215,21 @@ pub fn get_kind(token: Option) -> Operation { } } +/// Valida que solo se usen palabras clave permitidas en una consulta SQL. +/// +/// # Parámetros +/// +/// - `allowed`: Un arreglo de palabras clave permitidas. +/// - `tokens`: Una lista de tokens procesados. +/// - `operation`: El tipo de operación SQL para la validación. +/// +/// # Retorno +/// +/// Retorna un `Result` indicando si la validación fue exitosa. +/// +/// # Errores +/// +/// Retorna un error `Errored` si se encuentra una palabra clave no permitida. fn validate_keywords( allowed: &[&str], tokens: &VecDeque, @@ -136,6 +249,16 @@ fn validate_keywords( Ok(()) } +/// Lanza un error cuando se encuentra un token inesperado durante el análisis de una consulta. +/// +/// # Parámetros +/// +/// - `stage`: El nombre de la etapa de análisis en la que se encontró el token. +/// - `token`: El token inesperado encontrado. +/// +/// # Retorno +/// +/// Retorna un `Result` con un error `Errored`. pub fn unexpected_token_in_stage(stage: &str, token: &Token) -> Result<(), Errored> { errored!( Syntax, diff --git a/src/query/builder/select.rs b/src/query/builder/select.rs index 8bd9023..d516e94 100644 --- a/src/query/builder/select.rs +++ b/src/query/builder/select.rs @@ -12,15 +12,34 @@ const ALLOWED_KEYWORDS: &[&str] = &[ "SELECT", "FROM", "WHERE", "ORDER BY", "ASC", "DESC", "AND", "OR", "NOT", ]; +/// Esta estructura procesa los tokens de una consulta SQL y construye una consulta SELECT +/// con las columnas, la tabla, las condiciones y el orden especificados. pub struct SelectBuilder { tokens: VecDeque, } impl SelectBuilder { + /// Crea una nueva instancia de `SelectBuilder` con los tokens proporcionados. + /// + /// # Parámetros + /// - `tokens`: Un `VecDeque` que contiene los tokens de la consulta. + /// + /// # Retorna + /// - Una instancia de `SelectBuilder`. pub fn new(tokens: VecDeque) -> Self { Self { tokens } } + /// Analiza y extrae las expresiones de ordenamiento de la consulta. + /// + /// Este método procesa los tokens después de la cláusula `ORDER BY` y construye + /// una lista de ordenamientos basados en los campos y la dirección (ASC o DESC). + /// + /// # Retorna + /// - Un `Result` que contiene un vector de `Ordering` representando las expresiones de ordenamiento. + /// + /// # Errores + /// - Retorna un error si se encuentra un token inesperado en la fase de ordenamiento. fn parse_ordering(&mut self) -> Result, Errored> { let mut ordering = vec![]; while let Some(t) = self.tokens.pop_front() { @@ -47,6 +66,17 @@ impl SelectBuilder { } impl Builder for SelectBuilder { + /// Construye una consulta de tipo SELECT a partir de los tokens. + /// + /// Este método analiza los tokens para identificar las columnas, la tabla, + /// las condiciones y las expresiones de ordenamiento de la consulta. + /// + /// # Retorna + /// - Un `Result` que contiene la consulta `Query` si se construye exitosamente. + /// + /// # Errores + /// - Retorna un error si la consulta no está correctamente formada o si se detectan + /// palabras clave inválidas. fn build(&mut self) -> Result { let mut query = Query::default(); self.validate_keywords()?; @@ -66,10 +96,24 @@ impl Builder for SelectBuilder { Ok(query) } + /// Retorna una referencia mutable a los tokens que se están procesando. + /// + /// # Retorna + /// - Una referencia mutable a `VecDeque`. fn tokens(&mut self) -> &mut VecDeque { &mut self.tokens } + /// Valida que las palabras clave usadas en la consulta sean válidas para una consulta SELECT. + /// + /// Este método compara las palabras clave en los tokens con las permitidas para asegurarse + /// de que la consulta sea válida. + /// + /// # Retorna + /// - `Ok(())` si las palabras clave son válidas. + /// + /// # Errores + /// - Retorna un error si se detecta una palabra clave inválida en la consulta. fn validate_keywords(&self) -> Result<(), Errored> { validate_keywords(ALLOWED_KEYWORDS, &self.tokens, Select) } diff --git a/src/query/builder/update.rs b/src/query/builder/update.rs index e1fb887..37fa48c 100644 --- a/src/query/builder/update.rs +++ b/src/query/builder/update.rs @@ -12,15 +12,35 @@ use std::collections::VecDeque; const ALLOWED_KEYWORDS: &[&str] = &["SET", "WHERE", "AND", "OR"]; +/// Esta estructura procesa los tokens de una consulta SQL y permite construir una consulta +/// UPDATE con los valores a actualizar y las condiciones asociadas. pub struct UpdateBuilder { tokens: VecDeque, } impl UpdateBuilder { + /// Crea una nueva instancia de `UpdateBuilder` con los tokens proporcionados. + /// + /// # Parámetros + /// - `tokens`: Un `VecDeque` que contiene los tokens de la consulta. + /// + /// # Retorna + /// - Una instancia de `UpdateBuilder`. pub fn new(tokens: VecDeque) -> Self { Self { tokens } } + /// Analiza y extrae las expresiones de actualización de una consulta SQL UPDATE. + /// + /// Este método espera encontrar la palabra clave `SET` seguida de las expresiones que representan + /// las columnas y valores que se van a actualizar. + /// + /// # Retorna + /// - Un `Result` que contiene un vector de `ExpressionNode` representando las expresiones de actualización. + /// + /// # Errores + /// - Retorna un error si no se encuentra la palabra clave `SET` o si las expresiones de actualización + /// no están correctamente formadas. fn parse_updates(&mut self) -> Result, Errored> { self.pop_expecting("SET", Keyword)?; let mut updates = vec![]; @@ -44,6 +64,17 @@ impl UpdateBuilder { } impl Builder for UpdateBuilder { + /// Construye una consulta de tipo UPDATE a partir de los tokens. + /// + /// Este método analiza los tokens, identifica la tabla, las columnas y valores a actualizar, + /// y las condiciones de la consulta. + /// + /// # Retorna + /// - Un `Result` que contiene la consulta `Query` si se construye exitosamente. + /// + /// # Errores + /// - Retorna un error si la consulta no está correctamente formada o contiene palabras clave + /// inválidas. fn build(&mut self) -> Result { let mut query = Query::default(); self.validate_keywords()?; @@ -59,10 +90,24 @@ impl Builder for UpdateBuilder { Ok(query) } + /// Retorna una referencia mutable a los tokens que se están procesando. + /// + /// # Retorna + /// - Una referencia mutable a `VecDeque`. fn tokens(&mut self) -> &mut VecDeque { &mut self.tokens } + /// Valida que las palabras clave usadas en la consulta sean válidas para una consulta UPDATE. + /// + /// Este método compara las palabras clave en los tokens con las permitidas para asegurarse + /// de que la consulta sea válida. + /// + /// # Retorna + /// - `Ok(())` si las palabras clave son válidas. + /// + /// # Errores + /// - Retorna un error si se detecta una palabra clave inválida en la consulta. fn validate_keywords(&self) -> Result<(), Errored> { validate_keywords(ALLOWED_KEYWORDS, &self.tokens, Update) } diff --git a/src/query/executor/delete.rs b/src/query/executor/delete.rs index 8512618..8558918 100644 --- a/src/query/executor/delete.rs +++ b/src/query/executor/delete.rs @@ -7,6 +7,22 @@ use crate::utils::files::{ use std::io::{BufRead, BufReader, BufWriter, Write}; impl Executor { + /// Ejecuta la operación de eliminación de registros en la tabla especificada. + /// + /// # Proceso + /// + /// 1. Abre el archivo de la tabla especificada y crea un archivo temporal para escribir los registros que no serán eliminados. + /// 2. Lee el encabezado del archivo original y lo escribe en el archivo temporal. + /// 3. Procesa cada línea del archivo original: + /// - Divide la línea en campos y los convierte en una fila (`Row`). + /// - Verifica si la fila cumple con las condiciones de eliminación. + /// - Si la fila coincide con la condición de eliminación, la omite y no la escribe en el archivo temporal. + /// - Si no coincide con la condición de eliminación, escribe la línea original en el archivo temporal. + /// 4. Una vez procesadas todas las líneas, elimina el archivo original y renombra el archivo temporal para reemplazar el archivo original. + /// + /// # Errores + /// + /// Puede retornar un error si ocurre un problema al abrir los archivos, leer el encabezado, procesar las líneas o eliminar el archivo temporal. pub fn run_delete(&self) -> Result<(), Errored> { let table = get_table_file(&self.table_path)?; let (temp_table, temp_path) = get_temp_file(&self.query.table, &self.table_path)?; diff --git a/src/query/executor/insert.rs b/src/query/executor/insert.rs index 4f5561c..18a34c3 100644 --- a/src/query/executor/insert.rs +++ b/src/query/executor/insert.rs @@ -6,6 +6,21 @@ use crate::utils::files::{extract_header, get_table_file}; use std::io::{BufReader, Write}; impl Executor { + /// Ejecuta la operación de inserción de registros en la tabla especificada. + /// # Proceso + /// + /// 1. Abre el archivo de la tabla especificada en `self.table_path`. + /// 2. Lee el encabezado del archivo para obtener los nombres de las columnas. + /// 3. Asegura que el archivo termine en una nueva línea. + /// 4. Para cada inserción en `self.query.inserts`: + /// - Convierte los valores de la inserción en una fila de valores. + /// - Crea una nueva fila (`Row`) y la llena con los valores. + /// - Escribe la fila como una línea CSV en el archivo. + /// + /// # Errores + /// + /// Puede retornar un error si ocurre un problema al abrir el archivo de la tabla, leer el encabezado, + /// agregar la nueva línea o escribir en el archivo. pub fn run_insert(&self) -> Result<(), Errored> { let mut table = get_table_file(&self.table_path)?; let mut reader = BufReader::new(&table); diff --git a/src/query/executor/mod.rs b/src/query/executor/mod.rs index a5e6a69..99241d8 100644 --- a/src/query/executor/mod.rs +++ b/src/query/executor/mod.rs @@ -11,16 +11,66 @@ mod insert; mod select; mod update; +/// Ejecuta una consulta SQL en una tabla especificada. +/// +/// La estructura `Executor` es responsable de llevar a cabo la operación especificada en la consulta +/// SQL sobre la tabla indicada. Esta etapa del procesamiento se lleva acabo luego de haber tokenizado +/// y construido con coherencia sintáctica la consulta. +/// +/// Cualquier error que ocurra de ahora en adelante es causado por irregularidades entre la consulta +/// y el archivo destino. +/// +/// # Estructura +/// - `table_path`: Ruta del archivo de la tabla sobre la cual se ejecutará la consulta. +/// - `query`: La consulta SQL a ejecutar, representada como un objeto `Query`. pub struct Executor { table_path: PathBuf, query: Query, } impl Executor { + /// Crea una nueva instancia de `Executor`. + /// + /// # Argumentos + /// + /// - `table_path`: La ruta del archivo de la tabla sobre la cual se ejecutará la consulta. + /// - `query`: La consulta SQL a ejecutar, representada como un objeto `Query`. + /// + /// # Retorna + /// + /// Una nueva instancia de `Executor`. fn new(table_path: PathBuf, query: Query) -> Self { Executor { table_path, query } } + /// Ejecuta la consulta SQL especificada. + /// + /// Este método determina el tipo de operación (selección, actualización, eliminación, inserción) + /// basado en la consulta y llama al método correspondiente para realizar la operación. + /// + /// # Argumentos + /// + /// - `path`: Ruta al directorio donde se encuentran los archivos de las tablas. + /// - `query`: La consulta SQL a ejecutar. + /// + /// # Errores + /// + /// Retorna un error si la operación en la consulta no es reconocida o si ocurre un problema al + /// obtener la ruta de la tabla o al ejecutar la consulta. + /// + /// # Ejemplo + /// + /// ```rust + /// + /// use rustic_sql::query::executor::Executor; + /// use rustic_sql::query::structs::query::Query; + /// let query = Query::default(); + /// let result = Executor::run("path/to/tables", query); + /// match result { + /// Ok(()) => println!("Consulta ejecutada exitosamente."), + /// Err(e) => eprintln!("Error al ejecutar la consulta: {}", e), + /// } + /// ``` pub fn run(path: &str, query: Query) -> Result<(), Errored> { let table_path = get_table_path(Path::new(path), &query.table)?; let mut executor = Executor::new(table_path, query); diff --git a/src/query/executor/select.rs b/src/query/executor/select.rs index cee86b7..7e7b52d 100644 --- a/src/query/executor/select.rs +++ b/src/query/executor/select.rs @@ -11,6 +11,22 @@ use std::cmp::Ordering; use std::io::{BufRead, BufReader}; impl Executor { + /// Ejecuta la operación de selección de registros en la tabla especificada. + /// # Proceso + /// + /// 1. Abre el archivo de la tabla especificada. + /// 2. Lee el encabezado del archivo para obtener los nombres de las columnas. + /// 3. Valida las columnas de proyección especificadas en la consulta SQL. + /// 4. Lee y procesa cada línea del archivo: + /// - Divide la línea en campos y los convierte en una fila (`Row`). + /// - Verifica si la fila cumple con las condiciones de la consulta. + /// 5. Ordena las filas coincidentes según los criterios de ordenamiento. + /// 6. Imprime el encabezado y las filas coincidentes en la salida estándar. + /// + /// # Errores + /// + /// Puede retornar un error si ocurre un problema al abrir el archivo de la tabla, leer el encabezado, + /// procesar las líneas, validar las columnas de proyección o realizar el ordenamiento. pub fn run_select(&mut self) -> Result<(), Errored> { let table = get_table_file(&self.table_path)?; let mut reader = BufReader::new(&table); @@ -32,6 +48,19 @@ impl Executor { Ok(()) } + /// Ordena las filas coincidentes según los criterios de ordenamiento especificados en la consulta. + /// + /// Este método toma las filas coincidentes y las ordena en función de los campos y el tipo de ordenamiento + /// especificados en `self.query.ordering`. Verifica si los campos de ordenamiento existen en el encabezado + /// de la tabla y realiza la comparación correspondiente. + /// + /// # Errores + /// + /// Retorna un error si alguno de los campos de ordenamiento no existe en el encabezado. + /// + /// # Ejemplo + /// + /// Este método es llamado internamente por `run_select`, por lo que no tiene un ejemplo de uso independiente. fn sort_rows(&mut self, matched_rows: &mut [Row], header: &[String]) -> Result<(), Errored> { for order in &self.query.ordering { if !header.contains(&order.field.value) { @@ -58,12 +87,32 @@ impl Executor { Ok(()) } + /// Imprime las filas coincidentes en la salida estándar. + /// + /// Este método toma las filas coincidentes y las imprime en la salida estándar, proyectando solo las columnas + /// especificadas en la consulta SQL (`self.query.columns`). + /// + /// # Ejemplo + /// + /// Este método es llamado internamente por `run_select`, por lo que no tiene un ejemplo de uso independiente. fn output_rows(&self, matched_rows: &[Row]) { for row in matched_rows { row.print_projection(&self.query.columns) } } + /// Valida que todas las columnas especificadas en la proyección existan en el encabezado de la tabla. + /// + /// Este método verifica que todas las columnas que se desean proyectar en la consulta SQL (`self.query.columns`) + /// estén presentes en el encabezado del archivo de la tabla. Si alguna columna no existe, retorna un error. + /// + /// # Errores + /// + /// Retorna un error si alguna columna en la proyección no existe en el encabezado. + /// + /// # Ejemplo + /// + /// Este método es llamado internamente por `run_select`, por lo que no tiene un ejemplo de uso independiente. fn validate_projection(&self, header: &[String]) -> Result<(), Errored> { for column in &self.query.columns { let value = &column.value; diff --git a/src/query/executor/update.rs b/src/query/executor/update.rs index 7ff170d..92890f7 100644 --- a/src/query/executor/update.rs +++ b/src/query/executor/update.rs @@ -7,6 +7,22 @@ use crate::utils::files::{ use std::io::{BufRead, BufReader, BufWriter, Write}; impl Executor { + /// Ejecuta la operación de actualización de registros en la tabla especificada. + /// + /// # Proceso + /// + /// 1. Abre el archivo de la tabla especificada y crea un archivo temporal para escribir los registros actualizados. + /// 2. Lee el encabezado del archivo original y lo escribe en el archivo temporal. + /// 3. Procesa cada línea del archivo original: + /// - Divide la línea en campos y los convierte en una fila (`Row`). + /// - Verifica si la fila cumple con las condiciones de actualización. + /// - Si la fila coincide con la condición, aplica las actualizaciones especificadas en la consulta SQL (`self.query.updates`) y la escribe en el archivo temporal. + /// - Si no coincide, escribe la línea original en el archivo temporal. + /// 4. Una vez procesadas todas las líneas, elimina el archivo original y renombra el archivo temporal para reemplazar el archivo original. + /// + /// # Errores + /// + /// Puede retornar un error si ocurre un problema al abrir los archivos, leer el encabezado, procesar las líneas, aplicar las actualizaciones o eliminar el archivo temporal. pub fn run_update(&self) -> Result<(), Errored> { let table = get_table_file(&self.table_path)?; let (temp_table, temp_path) = get_temp_file(&self.query.table, &self.table_path)?; diff --git a/src/query/structs/comparator.rs b/src/query/structs/comparator.rs index 48ffa65..e355bc1 100644 --- a/src/query/structs/comparator.rs +++ b/src/query/structs/comparator.rs @@ -4,9 +4,21 @@ use crate::query::structs::expression::{ExpressionOperator, ExpressionResult}; use crate::utils::errors::Errored; use crate::utils::errors::Errored::Syntax; +/// Comparador de expresiones que permite comparar diferentes tipos de datos. +/// Es utilizado principalmente con los valores "hoja" de las expresiones SQL. pub struct ExpressionComparator; impl ExpressionComparator { + /// Compara dos enteros utilizando el operador especificado. + /// + /// # Parámetros + /// - `l`: El primer entero a comparar. + /// - `r`: El segundo entero a comparar. + /// - `op`: El operador de comparación a utilizar. + /// + /// # Retorno + /// Retorna un `Result` que contiene un `ExpressionResult` con el resultado de la comparación, + /// o un error `Errored` si el operador no es válido para enteros. pub fn compare_ints( l: i64, r: i64, @@ -23,6 +35,16 @@ impl ExpressionComparator { } } + /// Compara dos cadenas de texto utilizando el operador especificado. + /// + /// # Parámetros + /// - `l`: La primera cadena de texto a comparar. + /// - `r`: La segunda cadena de texto a comparar. + /// - `op`: El operador de comparación a utilizar. + /// + /// # Retorno + /// Retorna un `Result` que contiene un `ExpressionResult` con el resultado de la comparación, + /// o un error `Errored` si el operador no es válido para cadenas de texto. pub fn compare_str( l: &str, r: &str, @@ -39,6 +61,16 @@ impl ExpressionComparator { } } + /// Compara dos valores booleanos utilizando el operador especificado. + /// + /// # Parámetros + /// - `l`: El primer valor booleano a comparar. + /// - `r`: El segundo valor booleano a comparar. + /// - `op`: El operador de comparación a utilizar. + /// + /// # Retorno + /// Retorna un `Result` que contiene un `ExpressionResult` con el resultado de la comparación, + /// o un error `Errored` si el operador no es válido para valores booleanos. pub fn compare_bools( l: bool, r: bool, @@ -52,6 +84,15 @@ impl ExpressionComparator { } } + /// Compara dos resultados de expresiones para determinar su orden relativo. + /// + /// # Parámetros + /// - `this`: El primer resultado de expresión a comparar. + /// - `other`: El segundo resultado de expresión a comparar. + /// + /// # Retorno + /// Retorna un `Result` que contiene un valor `std::cmp::Ordering` que indica el orden relativo + /// de los dos resultados, o un error `Errored` si los tipos de los resultados no son comparables. pub fn compare_ordering( this: &ExpressionResult, other: &ExpressionResult, diff --git a/src/query/structs/expression.rs b/src/query/structs/expression.rs index 36bd6c7..69a030b 100644 --- a/src/query/structs/expression.rs +++ b/src/query/structs/expression.rs @@ -7,6 +7,10 @@ use crate::utils::errors::Errored::{Column, Default, Syntax}; use std::collections::HashMap; use std::fmt::{Debug, Formatter}; +/// Enum que representa a una expresión. +/// +/// Usando una estructura recursiva de nodos, el mismo puede ser un nodo vacío, una hoja +/// con un token, o una declaración con un operador y dos sub-nodos (izquierdo y derecho). #[derive(Default, PartialEq)] pub enum ExpressionNode { #[default] @@ -19,6 +23,10 @@ pub enum ExpressionNode { }, } +/// Enum que define los operadores posibles en una expresión. +/// +/// Los operadores incluyen comparación (igual, mayor.. etc.) y operadores +/// lógicos (AND, OR, NOT). #[derive(Debug, Default, PartialEq)] pub enum ExpressionOperator { #[default] @@ -34,6 +42,9 @@ pub enum ExpressionOperator { Not, } +/// Enum que representa los posibles resultados de una expresión. +/// +/// Los resultados pueden ser un entero, un string o un valor booleano. #[derive(Debug, PartialEq)] pub enum ExpressionResult { Int(i64), @@ -42,6 +53,18 @@ pub enum ExpressionResult { } impl ExpressionNode { + /// Evalúa el nodo de expresión usando los valores proporcionados. + /// Dichos valores estan contenidos dentro de un mapa que representa el contexto actual + /// de la ejecución. + /// + /// # Parámetros + /// + /// * `values` - Un `HashMap` que contiene los pares clave, valor del contexto actual. + /// + /// # Retorna + /// + /// Un `Result` que contiene el resultado de la evaluación de la expresión o un error en caso de + /// que ocurra algún problema. pub fn evaluate(&self, values: &HashMap) -> Result { match self { ExpressionNode::Empty => Ok(Bool(true)), @@ -63,6 +86,20 @@ impl ExpressionNode { } } + /// Obtiene el valor de una declaración comparativa. + /// El método comparativo a ser ejecutado depende de los tipos de datos contenidos + /// en las hojas de la expresión. + /// + /// # Parámetros + /// + /// * `operator` - El operador de la expresión. + /// * `left` - El resultado de la evaluación del lado izquierdo de la expresión. + /// * `right` - El resultado de la evaluación del lado derecho de la expresión. + /// + /// # Retorna + /// + /// Un `Result` que contiene el resultado de la comparación o un error en caso de que los tipos + /// no coincidan. fn get_statement_value( operator: &ExpressionOperator, left: ExpressionResult, @@ -76,6 +113,18 @@ impl ExpressionNode { } } + /// Obtiene el valor de una variable a partir del `HashMap` de valores. + /// Dicho `HashMap`vendría a ser el contexto en donde se esta interprentando la + /// expresión. + /// + /// # Parámetros + /// + /// * `values` - Un `HashMap` que contiene los pares clave, valor del contexto actual. + /// * `t` - El token que representa la variable. + /// + /// # Retorna + /// + /// Un `Result` que contiene el valor de la variable o un error si la variable no existe. pub fn get_variable_value( values: &HashMap, t: &Token, @@ -93,6 +142,14 @@ impl ExpressionNode { } } + /// Obtiene una tupla de los tokens de una declaración que son hojas. + /// Este método es usado para representar las actualizaciones de una consulta. + /// Ya que una actualización tiene una llave y un valor, nos conviene devolver en un par. + /// + /// # Retorna + /// + /// Un `Result` que contiene una tupla con los dos tokens de las hojas o un error si los nodos + /// no son hojas. pub fn as_leaf_tuple(&self) -> Result<(&Token, &Token), Errored> { match self { ExpressionNode::Statement { left, right, .. } => match (&**left, &**right) { diff --git a/src/query/structs/operation.rs b/src/query/structs/operation.rs index bb50f60..890a855 100644 --- a/src/query/structs/operation.rs +++ b/src/query/structs/operation.rs @@ -1,3 +1,12 @@ +/// Enum que representa las diferentes operaciones que se pueden realizar dentro de RusticSQL. +/// +/// Las operaciones incluyen: +/// +/// - `Unknown`: La operación es desconocida o no se ha especificado. +/// - `Select`: Selecciona datos de una tabla. +/// - `Update`: Actualiza datos existentes en una tabla. +/// - `Delete`: Elimina datos de una tabla. +/// - `Insert`: Inserta nuevos datos en una tabla. #[derive(Debug, PartialEq)] pub enum Operation { Unknown, diff --git a/src/query/structs/ordering.rs b/src/query/structs/ordering.rs index b1b278d..e034401 100644 --- a/src/query/structs/ordering.rs +++ b/src/query/structs/ordering.rs @@ -2,12 +2,26 @@ use crate::query::structs::ordering::OrderKind::Asc; use crate::query::structs::token::Token; use std::fmt::{Debug, Formatter}; +/// Estructura que representa un criterio de ordenamiento dentro de RusticSQL. +/// +/// Esta estructura se utiliza para definir cómo se deben ordenar los resultados en una consulta. +/// +/// # Campos +/// +/// * `field` - El token que representa el campo por el cual se realizará el ordenamiento. +/// * `kind` - El tipo de ordenamiento (ascendente o descendente). #[derive(PartialEq)] pub struct Ordering { pub field: Token, pub kind: OrderKind, } +/// Enum que representa los tipos de ordenamiento posibles. +/// +/// Los tipos de ordenamiento incluyen: +/// +/// - `Asc`: Ordena los resultados de manera ascendente. +/// - `Desc`: Ordena los resultados de manera descendente. #[derive(Debug, PartialEq)] pub enum OrderKind { Asc, @@ -15,6 +29,9 @@ pub enum OrderKind { } impl Default for Ordering { + /// Devuelve un valor default para `Ordering`. + /// + /// El valor default utiliza el token predeterminado y el tipo de ordenamiento ascendente. fn default() -> Self { Self { field: Token::default(), diff --git a/src/query/structs/query.rs b/src/query/structs/query.rs index 4d3cd4e..a4b4e33 100644 --- a/src/query/structs/query.rs +++ b/src/query/structs/query.rs @@ -14,22 +14,47 @@ use crate::utils::errors::Errored::Syntax; use std::collections::VecDeque; use std::fmt::{Debug, Display, Formatter}; +/// Estructura que representa una consulta dentro de RusticSQL. +/// +/// La consulta incluye la operación a realizar, la tabla, las columnas, los valores para insertar, +/// las actualizaciones, las condiciones y el ordenamiento, algunos de estos campos pueden quedar +/// con valores default en caso de no aplicar. pub struct Query { + /// La operación que se debe realizar. pub operation: Operation, + /// La tabla sobre la que se realiza la operación. pub table: String, + /// Las columnas involucradas en la consulta. pub columns: Vec, + /// Los valores a insertar en caso de una operación de inserción. pub inserts: Vec>, + /// Las actualizaciones a realizar en caso de una operación de actualización. pub updates: Vec, + /// Las condiciones para filtrar los resultados de la consulta. pub conditions: ExpressionNode, + /// El criterio de ordenamiento para los resultados. pub ordering: Vec, } impl Query { + /// Crea una nueva consulta a partir de una lista de tokens. + /// + /// La función intenta identificar el tipo de operación + /// y construir la consulta correspondiente usando el builder adecuado. + /// + /// # Parámetros + /// + /// * `tokens` - La lista de tokens obtenida de tokenizar un string que representaba la consulta. + /// + /// # Retorna + /// + /// Un `Result` que contiene la consulta construida o un error en caso de que la consulta no sea + /// válida. pub fn from(tokens: Vec) -> Result { let mut tokens = VecDeque::from(tokens); let kind = get_kind(tokens.pop_front()); match kind { - Unknown => errored!(Syntax, "query does not start with a valid operation."), + Unknown => errored!(Syntax, "la consulta no comienza con una operación válida."), Select => SelectBuilder::new(tokens).build(), Update => UpdateBuilder::new(tokens).build(), Delete => DeleteBuilder::new(tokens).build(), @@ -39,6 +64,7 @@ impl Query { } impl Default for Query { + /// Devuelve un valor default para `Query`. fn default() -> Self { Self { operation: Unknown, @@ -55,25 +81,25 @@ impl Default for Query { impl Display for Query { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let fields: Vec<&str> = self.columns.iter().map(|f| f.value.as_str()).collect(); - writeln!(f, "Query Kind: [{:?}]", self.operation)?; - writeln!(f, "Table: {:?}", self.table)?; - writeln!(f, "Columns: {:?}", fields)?; + writeln!(f, "Tipo de Consulta: [{:?}]", self.operation)?; + writeln!(f, "Tabla: {:?}", self.table)?; + writeln!(f, "Columnas: {:?}", fields)?; writeln!(f, "Inserts {{ ")?; for insert in &self.inserts { let values: Vec<&String> = insert.iter().map(|t| &t.value).collect(); writeln!(f, " {:?}", values)?; } writeln!(f, "}} ")?; - writeln!(f, "Updates {{ ")?; + writeln!(f, "Actualizaciones {{ ")?; for up in &self.updates { if let Ok((l, r)) = up.as_leaf_tuple() { writeln!(f, " {} -> {}", l.value, r.value)?; } } writeln!(f, "}} ")?; - writeln!(f, "Updates: {:?}", self.updates)?; - writeln!(f, "Conditions: {:?}", self.conditions)?; - writeln!(f, "Ordering: {:?}", self.ordering) + writeln!(f, "Actualizaciones: {:?}", self.updates)?; + writeln!(f, "Condiciones: {:?}", self.conditions)?; + writeln!(f, "Ordenamiento: {:?}", self.ordering) } } @@ -93,10 +119,13 @@ mod test { fn test_invalid_query() { let tokens = vec![Token::default()]; let result = Query::from(tokens); - assert!(result.is_err(), "should be errored with unknown token."); + assert!( + result.is_err(), + "debería dar error con un token desconocido." + ); match result { - Err(Errored::Syntax(msg)) => assert!(msg.contains("valid operation")), - _ => panic!("expected syntax error for query starting keyword."), + Err(Errored::Syntax(msg)) => assert!(msg.contains("operación válida")), + _ => panic!("se esperaba un error de sintaxis para el primer token de la consulta."), } } } diff --git a/src/query/structs/row.rs b/src/query/structs/row.rs index 2a021ea..f6ebe6e 100644 --- a/src/query/structs/row.rs +++ b/src/query/structs/row.rs @@ -6,12 +6,22 @@ use crate::utils::errors::Errored; use crate::utils::errors::Errored::{Column, Default, Syntax, Table}; use std::collections::HashMap; +/// Representa una fila en una tabla, con un encabezado y valores asociados. pub struct Row<'a> { pub header: &'a Vec, pub values: HashMap, } impl<'a> Row<'a> { + /// Crea una nueva instancia de `Row` con un encabezado dado. + /// + /// # Ejemplo + /// + /// ```rust + /// use rustic_sql::query::structs::row::Row; + /// let header = vec!["id".to_string(), "apellido".to_string()]; + /// let row = Row::new(&header); + /// ``` pub fn new(header: &'a Vec) -> Self { Self { header, @@ -19,7 +29,26 @@ impl<'a> Row<'a> { } } - fn set(&mut self, key: &str, value: String) -> Result<(), Errored> { + /// Establece un valor para una columna en la fila. + /// + /// # Parámetros + /// + /// - `key`: El nombre de la columna. + /// - `value`: El valor a asignar. + /// + /// # Errores + /// + /// Devuelve un error si la columna no existe en el encabezado. + /// + /// # Ejemplo + /// + /// ```rust + /// use rustic_sql::query::structs::row::Row; + /// let header = vec!["id".to_string(), "apellido".to_string()]; + /// let mut row = Row::new(&header); + /// row.set("id", "123".to_string()).unwrap(); + /// ``` + pub fn set(&mut self, key: &str, value: String) -> Result<(), Errored> { if self.header.contains(&key.to_string()) { self.values.insert(key.to_string(), value); } else { @@ -33,6 +62,17 @@ impl<'a> Row<'a> { Ok(()) } + /// Limpia los valores de la fila, estableciendo cada columna con un string vacío. + /// + /// # Ejemplo + /// + /// ```rust + /// use rustic_sql::query::structs::row::Row; + /// let header = vec!["id".to_string(), "apellido".to_string()]; + /// let mut row = Row::new(&header); + /// row.set("id", "123".to_string()).unwrap(); + /// row.clear().unwrap(); + /// ``` pub fn clear(&mut self) -> Result<(), Errored> { for key in self.header { self.set(key, "".to_string())? @@ -40,6 +80,38 @@ impl<'a> Row<'a> { Ok(()) } + /// Aplica una lista de actualizaciones a la fila. + /// + /// # Parámetros + /// + /// - `updates`: Lista de expresiones que representan las actualizaciones. + /// + /// # Errores + /// + /// Devuelve un error si ocurre un problema al aplicar las actualizaciones. + /// + /// # Ejemplo + /// + /// ```rust + /// use rustic_sql::query::structs::expression::{ExpressionNode, ExpressionOperator}; + /// use rustic_sql::query::structs::row::Row; + /// use rustic_sql::query::structs::token::Token; + /// use rustic_sql::query::structs::token::TokenKind::{Identifier, String}; + /// let header = vec!["id".to_string(), "apellido".to_string()]; + /// let mut row = Row::new(&header); + /// let update = ExpressionNode::Statement { + /// operator: ExpressionOperator::Equals, + /// left: Box::new(ExpressionNode::Leaf(Token { + /// kind: Identifier, + /// value: "id".to_string(), + /// })), + /// right: Box::new(ExpressionNode::Leaf(Token { + /// kind: String, + /// value: "360".to_string(), + /// })), + /// }; + /// row.apply_updates(&vec![update]).unwrap(); + /// ``` pub fn apply_updates(&mut self, updates: &Vec) -> Result<(), Errored> { for up in updates { if let Ok((field, value)) = up.as_leaf_tuple() { @@ -53,6 +125,26 @@ impl<'a> Row<'a> { Ok(()) } + /// Lee una nueva fila con los valores proporcionados. + /// + /// # Parámetros + /// + /// - `values`: Valores a insertar en la fila. + /// + /// # Errores + /// + /// Devuelve un error si el número de valores no coincide con el número de columnas. + /// Tambien devuelve error si llega a fallar la inserción. + /// + /// # Ejemplo + /// + /// ```rust + /// use rustic_sql::query::structs::row::Row; + /// let header = vec!["id".to_string(), "apellido".to_string()]; + /// let mut row = Row::new(&header); + /// let values = vec!["360".to_string(), "katta".to_string()]; + /// row.read_new_row(values).unwrap(); + /// ``` pub fn read_new_row(&mut self, values: Vec) -> Result<(), Errored> { if self.header.len() != values.len() { errored!( @@ -68,6 +160,38 @@ impl<'a> Row<'a> { Ok(()) } + /// Inserta valores en columnas específicas. + /// + /// # Parámetros + /// + /// - `columns`: Lista de columnas en las que insertar los valores. + /// - `values`: Valores a insertar. + /// + /// # Errores + /// + /// Devuelve un error si alguna columna no existe. + /// + /// # Ejemplo + /// + /// ```rust + /// use rustic_sql::query::structs::row::Row; + /// use rustic_sql::query::structs::token::Token; + /// use rustic_sql::query::structs::token::TokenKind::Identifier; + /// let header = vec!["id".to_string(), "apellido".to_string()]; + /// let mut row = Row::new(&header); + /// let columns = vec![ + /// Token { + /// kind: Identifier, + /// value: "id".to_string(), + /// }, + /// Token { + /// kind: Identifier, + /// value: "apellido".to_string(), + /// }, + /// ]; + /// let values = vec!["360".to_string(), "katta".to_string()]; + /// row.insert_values(&columns, values).unwrap(); + /// ``` pub fn insert_values(&mut self, columns: &[Token], values: Vec) -> Result<(), Errored> { for (col, value) in columns.iter().zip(values) { self.set(&col.value, value)? @@ -75,7 +199,24 @@ impl<'a> Row<'a> { Ok(()) } - fn as_csv_projection(&self, fields: &Vec) -> String { + /// Convierte la fila en un string CSV con campos específicos. + /// + /// # Parámetros + /// + /// - `fields`: Lista de campos a incluir en la proyección CSV. + /// + /// # Ejemplo + /// + /// ```rust + /// use rustic_sql::query::structs::row::Row; + /// let header = vec!["id".to_string(), "apellido".to_string()]; + /// let mut row = Row::new(&header); + /// row.set("id", "360".to_string()).unwrap(); + /// row.set("apellido", "katta".to_string()).unwrap(); + /// let csv_string = row.as_csv_projection(&vec!["id".to_string(), "apellido".to_string()]); + /// assert_eq!(csv_string, "360,katta"); + /// ``` + pub fn as_csv_projection(&self, fields: &Vec) -> String { let mut projection: Vec<&str> = Vec::new(); for key in fields { let value = self.values.get(key).map(|v| v.as_str()).unwrap_or(""); @@ -84,10 +225,41 @@ impl<'a> Row<'a> { projection.join(",") } + /// Convierte la fila completa en un string CSV. + /// + /// # Ejemplo + /// + /// ```rust + /// use rustic_sql::query::structs::row::Row; + /// let header = vec!["id".to_string(), "apellido".to_string()]; + /// let mut row = Row::new(&header); + /// row.set("id", "360".to_string()).unwrap(); + /// row.set("apellido", "katta".to_string()).unwrap(); + /// let csv_string = row.as_csv_row(); + /// assert_eq!(csv_string, "360,katta"); + /// ``` pub fn as_csv_row(&self) -> String { self.as_csv_projection(self.header) } + /// Imprime los valores de la fila según las columnas especificadas. + /// + /// # Parámetros + /// + /// - `columns`: Lista de columnas para imprimir. + /// + /// # Ejemplo + /// + /// ```rust + /// use rustic_sql::query::structs::row::Row; + /// use rustic_sql::query::structs::token::Token; + /// use rustic_sql::query::structs::token::TokenKind::Identifier; + /// let header = vec!["id".to_string(), "apellido".to_string()]; + /// let mut row = Row::new(&header); + /// row.set("id", "360".to_string()).unwrap(); + /// row.set("apellido", "katta".to_string()).unwrap(); + /// row.print_projection(&vec![Token { kind: Identifier, value: "id".to_string() }]); + /// ``` pub fn print_projection(&self, columns: &[Token]) { if columns.is_empty() { println!("{}", self.as_csv_row()); @@ -97,6 +269,51 @@ impl<'a> Row<'a> { } } + /// Verifica si la fila cumple con la condición especificada en la consulta. + /// + /// Evalúa la condición de la consulta utilizando los valores actuales de la fila. + /// Si la evaluación resulta en un valor booleano, se devuelve este valor. + /// Si el resultado no es booleano, se devuelve un error de sintaxis. + /// + /// # Parámetros + /// + /// - `query`: La consulta que contiene la condición que se debe evaluar. + /// + /// # Errores + /// + /// Devuelve un error si la evaluación de la condición no resulta en un valor booleano. + /// + /// # Ejemplo + /// + /// ```rust + /// use rustic_sql::query::structs::expression::{ExpressionNode, ExpressionOperator}; + /// use rustic_sql::query::structs::query::Query; + /// use rustic_sql::query::structs::row::Row; + /// use rustic_sql::query::structs::token::Token; + /// use rustic_sql::query::structs::token::TokenKind::{Identifier, Number}; + /// let header = vec!["id".to_string()]; + /// let mut row = Row::new(&header); + /// row.set("id", "365".to_string()).unwrap(); + /// + /// let condition = ExpressionNode::Statement { + /// operator: ExpressionOperator::GreaterThan, + /// left: Box::new(ExpressionNode::Leaf(Token { + /// kind: Identifier, + /// value: "id".to_string(), + /// })), + /// right: Box::new(ExpressionNode::Leaf(Token { + /// kind: Number, + /// value: "360".to_string(), + /// })), + /// }; + /// + /// let query = Query { + /// conditions: condition, + /// ..Query::default() + /// }; + /// + /// assert!(row.matches_condition(&query).unwrap()); + /// ``` pub fn matches_condition(&self, query: &Query) -> Result { match query.conditions.evaluate(&self.values)? { ExpressionResult::Bool(b) => Ok(b), diff --git a/src/query/structs/token.rs b/src/query/structs/token.rs index e23bbf3..85f38b6 100644 --- a/src/query/structs/token.rs +++ b/src/query/structs/token.rs @@ -1,11 +1,51 @@ use crate::query::structs::token::TokenKind::Unknown; +/// Representa un token en una consulta. +/// +/// Un token tiene un valor y un tipo (kind) que define su función o significado en la consulta. +/// +/// # Ejemplo +/// +/// ```rust +/// use rustic_sql::query::structs::token::{Token, TokenKind}; +/// +/// let token = Token { +/// value: "id_cliente".to_string(), +/// kind: TokenKind::Identifier, +/// }; +/// assert_eq!(token.value, "id_cliente"); +/// assert_eq!(token.kind, TokenKind::Identifier); +/// ``` #[derive(Debug, PartialEq)] pub struct Token { + /// El valor del token como un string. pub value: String, + + /// El tipo de token que define su función o significado. pub kind: TokenKind, } +/// Enum que define los posibles tipos de un token. +/// +/// Cada variante representa un tipo diferente de token que puede aparecer en una consulta. +/// +/// - `Unknown`: Un tipo de token no reconocido. +/// - `String`: Un token que representa una cadena de texto. +/// - `Number`: Un token que representa un número. +/// - `Operator`: Un token que representa un operador. +/// - `Identifier`: Un token que representa un identificador o variable. +/// - `ParenthesisOpen`: Un token que representa un paréntesis de apertura. +/// - `ParenthesisClose`: Un token que representa un paréntesis de cierre. +/// - `Keyword`: Un token que representa una palabra clave de SQL. +/// +/// # Ejemplo +/// +/// ```rust +/// use rustic_sql::query::structs::token::TokenKind; +/// +/// let kind = TokenKind::String; +/// assert_eq!(kind, TokenKind::String); +/// ``` #[derive(Debug, PartialEq)] pub enum TokenKind { Unknown, @@ -19,6 +59,7 @@ pub enum TokenKind { } impl Default for Token { + /// Crea un token con valores predeterminados. fn default() -> Self { Self { value: String::new(), diff --git a/src/query/tokenizer.rs b/src/query/tokenizer.rs index ccfa045..c2a0891 100644 --- a/src/query/tokenizer.rs +++ b/src/query/tokenizer.rs @@ -28,6 +28,20 @@ const RESERVED_KEYWORDS: &[&str] = &[ "NOT", ]; +/// `Tokenizer` es una estructura que se encarga de analizar y tokenizar un string SQL. +/// +/// Esta estructura divide un string SQL en tokens basados en los componentes del SQL, como palabras clave, +/// identificadores, operadores, literales numéricos y de cadena, y paréntesis. +/// +/// # Ejemplo +/// +/// ```rust +/// use rustic_sql::query::tokenizer::Tokenizer; +/// let sql = "SELECT id FROM table WHERE age > 30"; +/// let mut tokenizer = Tokenizer::new(); +/// let tokens = tokenizer.tokenize(sql).unwrap(); +/// println!("{:?}", tokens); +/// ``` #[derive(Default)] pub struct Tokenizer { i: usize, @@ -35,6 +49,18 @@ pub struct Tokenizer { parenthesis_count: i8, } +/// `TokenizerState` representa los posibles estados del `Tokenizer` durante el proceso de tokenización. +/// +/// Estos estados ayudan a determinar cómo se debe analizar el siguiente carácter en el string SQL. +/// +/// - `Begin`: Estado inicial antes de comenzar el análisis. +/// - `IdentifierOrKeyword`: Estado cuando se está analizando un identificador o una palabra clave. +/// - `Operator`: Estado cuando se está analizando un operador. +/// - `NumberLiteral`: Estado cuando se está analizando un literal numérico. +/// - `StringLiteral`: Estado cuando se está analizando una cadena de texto. +/// - `OpenParenthesis`: Estado cuando se está analizando un paréntesis de apertura. +/// - `CloseParenthesis`: Estado cuando se está analizando un paréntesis de cierre. +/// - `Complete`: Estado cuando el token actual ha sido completado. #[derive(Default)] enum TokenizerState { #[default] @@ -49,6 +75,14 @@ enum TokenizerState { } impl Tokenizer { + /// Crea una nueva instancia de `Tokenizer`. + /// + /// # Ejemplo + /// + /// ```rust + /// use rustic_sql::query::tokenizer::Tokenizer; + /// let tokenizer = Tokenizer::new(); + /// ``` pub fn new() -> Self { Self { i: 0, @@ -57,6 +91,21 @@ impl Tokenizer { } } + /// Tokeniza un string SQL en una lista de `Token`. + /// + /// # Ejemplo + /// + /// ```rust + /// use rustic_sql::query::tokenizer::Tokenizer; + /// let sql = "SELECT id FROM table WHERE age > 30"; + /// let mut tokenizer = Tokenizer::new(); + /// let tokens = tokenizer.tokenize(sql).unwrap(); + /// println!("{:?}", tokens); + /// ``` + /// + /// # Errores + /// + /// Retorna un error si se encuentra un carácter no reconocido o si hay paréntesis no balanceados. pub fn tokenize(&mut self, sql: &str) -> Result, Errored> { let mut out = vec![]; let mut token = Token::default(); @@ -88,6 +137,11 @@ impl Tokenizer { Ok(out) } + /// Cambia el estado del `Tokenizer` basado en el carácter actual. + /// + /// # Errores + /// + /// Retorna un error si el carácter no se puede tokenizar. fn next_state(&mut self, c: char) -> Result<(), Errored> { match c { c if can_be_skipped(c) => self.i += c.len_utf8(), @@ -107,6 +161,13 @@ impl Tokenizer { Ok(()) } + /// Tokeniza un paréntesis, ya sea de apertura o de cierre. + /// + /// Dependiendo del carácter, el token se establece como `ParenthesisOpen` o `ParenthesisClose`. + /// + /// # Errores + /// + /// Retorna un error si el carácter no es un paréntesis reconocido. fn tokenize_parenthesis(&mut self, sql: &str) -> Result { let c = char_at(self.i, sql); let mut token = Token::default(); @@ -126,6 +187,14 @@ impl Tokenizer { Ok(token) } + /// Tokeniza un identificador o una palabra clave. + /// + /// Si el texto coincide con una palabra clave reservada, se tokeniza como `Keyword`. De lo contrario, + /// se tokeniza como `Identifier`. + /// + /// # Errores + /// + /// Retorna un error si el texto no puede ser tokenizado. fn tokenize_identifier_or_keyword(&mut self, sql: &str) -> Result { if let Some(word) = self.matches_keyword(sql) { self.i += word.len(); @@ -138,10 +207,24 @@ impl Tokenizer { self.tokenize_kind(sql, Identifier, is_identifier_char) } + /// Tokeniza un literal numérico. + /// + /// Se tokeniza el texto como `Number` si corresponde a un literal numérico. + /// + /// # Errores + /// + /// Retorna un error si el texto no puede ser tokenizado. fn tokenize_number(&mut self, sql: &str) -> Result { self.tokenize_kind(sql, Number, |c| c.is_ascii_digit()) } + /// Tokeniza un operador. + /// + /// Se tokeniza el texto como `Operator` si corresponde a un operador válido. + /// + /// # Errores + /// + /// Retorna un error si el operador no es reconocido. fn tokenize_operator(&mut self, sql: &str) -> Result { if let Some(op) = self.matches_operator(sql) { self.i += op.len(); @@ -160,6 +243,13 @@ impl Tokenizer { } } + /// Tokeniza una cadena de texto entre comillas simples. + /// + /// Extrae el contenido entre las comillas simples y lo tokeniza como `String`. + /// + /// # Errores + /// + /// Retorna un error si las comillas no están balanceadas. fn tokenize_quoted(&mut self, sql: &str) -> Result { let start = self.i; for (index, char) in sql[start..].char_indices() { @@ -177,14 +267,32 @@ impl Tokenizer { errored!(Syntax, "unclosed quotation mark after index: {start}"); } + /// Busca una palabra clave en el string SQL. + /// + /// Verifica si el texto actual coincide con alguna de las palabras clave reservadas. + /// fn matches_keyword(&self, sql: &str) -> Option { self.matches_special_tokens(sql, RESERVED_KEYWORDS, is_identifier_char) } + /// Busca un operador en la cadena SQL. + /// + /// Verifica si el texto actual coincide con algún operador válido. + /// fn matches_operator(&self, sql: &str) -> Option { self.matches_special_tokens(sql, VALID_OPERATORS, is_operator_char) } + /// Busca tokens especiales en la consulta SQL. + /// + /// Compara el texto actual con una lista de tokens especiales (palabras clave, operadores) y verifica si + /// el siguiente carácter no coincide con el tipo esperado. + /// + /// En caso de que el siguiente caracter a el token evaluado actualmente no coincida en tipo, + /// es un indicador de que ya podemos dejar de matchear el token. + /// + /// + /// fn matches_special_tokens( &self, sql: &str, @@ -208,6 +316,10 @@ impl Tokenizer { None } + /// Tokeniza un tipo de dato específico (identificador, número, etc.) en la cadena SQL. + /// + /// Se tokeniza el texto como el tipo de dato especificado si el carácter actual coincide con el tipo de + /// dato esperado. fn tokenize_kind( &mut self, sql: &str, @@ -231,23 +343,38 @@ impl Tokenizer { }) } + /// Restablece el estado del `Tokenizer` al estado inicial. + /// + /// Esto se usa para preparar el `Tokenizer` para el próximo token después de completar el actual. fn reset(&mut self) { self.state = Begin } } +/// Obtiene el carácter en el índice dado de un string. fn char_at(index: usize, string: &str) -> char { string[index..].chars().next().unwrap_or('\0') } +/// Determina si un carácter puede ser ignorado. +/// +/// Los caracteres ignorables son aquellos que no afectan el análisis del SQL, +/// como espacios y delimitadores. fn can_be_skipped(c: char) -> bool { c.is_whitespace() || IGNORABLE_CHARS.contains(&c) } +/// Determina si un carácter es válido para un identificador o variable. +/// +/// Los identificadores pueden comenzar con letras o guiones bajos, seguidos +/// de letras, dígitos o guiones bajos. fn is_identifier_char(c: char) -> bool { c == '_' || (c.is_alphanumeric() && !can_be_skipped(c)) } +/// Determina si un carácter es un operador válido. +/// +/// Los operadores válidos son aquellos definidos en `VALID_OPERATORS`. fn is_operator_char(c: char) -> bool { VALID_OPERATORS.contains(&c.to_string().as_str()) } diff --git a/src/utils/errors.rs b/src/utils/errors.rs index 4b27ef8..f02cd58 100644 --- a/src/utils/errors.rs +++ b/src/utils/errors.rs @@ -4,6 +4,27 @@ use std::fmt::{Debug, Display, Formatter}; use std::io; use std::num::ParseIntError; +/// Macro para crear errores personalizados con los errores `Errored`. +/// +/// Este macro facilita la creación de errores personalizados al construir un `Errored` +/// con un mensaje formateado, una vez creado el error la misma macro lo retorna. +/// +/// # Ejemplo +/// +/// ```rust +/// +/// use rustic_sql::errored; +/// use rustic_sql::utils::errors::Errored; +/// use rustic_sql::utils::errors::Errored::Syntax; +/// +/// fn example_function() -> Result<(), Errored> { +/// let some_condition = true; +/// if some_condition { +/// errored!(Syntax, "An error occurred with syntax"); +/// } +/// Ok(()) +/// } +/// ``` #[macro_export] macro_rules! errored { ($err_type:ident, $msg:expr) => { @@ -14,7 +35,18 @@ macro_rules! errored { }; } -/// Generic Error for the RusticSQL Application. +/// Enum que representa diferentes tipos de errores en la aplicación RusticSQL. +/// +/// Este enum permite clasificar los errores que pueden ocurrir durante el procesamiento de +/// datos o la ejecución de operaciones, proporcionando mensajes descriptivos para cada tipo de error. +/// +/// # Variantes +/// +/// - `Syntax(String)`: Representa un error relacionado con la sintaxis. +/// - `Column(String)`: Representa un error relacionado con una columna. +/// - `Table(String)`: Representa un error relacionado con una tabla. +/// - `Default(String)`: Representa un error genérico. +/// pub enum Errored { Syntax(String), Column(String), diff --git a/src/utils/files.rs b/src/utils/files.rs index 2106612..73a2a4e 100644 --- a/src/utils/files.rs +++ b/src/utils/files.rs @@ -12,18 +12,56 @@ const TEMP_EXTENSION: &str = "tmp"; const CSV_EXTENSION: &str = "csv"; const CSV_SEPARATOR: &str = ","; +/// Extrae el encabezado de un archivo CSV. +/// +/// # Parámetros +/// +/// - `reader`: Un `BufReader` que envuelve un `File` desde el cual leer el encabezado. +/// +/// # Retorna +/// +/// Devuelve un `Result` que contiene un `Vec` con los nombres de las columnas si tiene éxito, o un `Errored` en caso de error. pub fn extract_header(reader: &mut BufReader<&File>) -> Result, Errored> { let mut header = String::new(); reader.read_line(&mut header)?; Ok(split_csv(&header)) } +/// Divide una línea CSV en un vector de strings. +/// +/// # Parámetros +/// +/// - `line`: La línea CSV que se desea dividir. +/// +/// # Retorna +/// +/// Devuelve un `Vec` con los valores separados. +/// +/// # Ejemplo +/// +/// ```rust +/// use rustic_sql::utils::files::split_csv; +/// let line = "id, id_cliente , email "; +/// let result = split_csv(line); +/// println!("{:?}", result); // Imprime ["id", "id_cliente", "email"] +/// ``` pub fn split_csv(line: &str) -> Vec { line.split(CSV_SEPARATOR) .map(|s| s.trim().to_string()) .collect::>() } +/// Obtiene la ruta completa del archivo CSV para una tabla dada. +/// +/// # Parámetros +/// +/// - `dir_path`: El directorio donde buscar. +/// - `table_name`: El nombre de la tabla. +/// +/// # Retorna +/// +/// Devuelve un `Result` que contiene un `PathBuf` con la ruta al archivo de la tabla si tiene éxito, o un `Errored` en caso de error. +/// ``` pub fn get_table_path(dir_path: &Path, table_name: &str) -> Result { let table_path = dir_path.join(table_name).with_extension(CSV_EXTENSION); if !table_path.is_file() { @@ -37,6 +75,19 @@ pub fn get_table_path(dir_path: &Path, table_name: &str) -> Result u64 { let id = thread::current().id(); let mut hasher = DefaultHasher::new(); @@ -44,6 +95,16 @@ pub fn get_temp_id() -> u64 { hasher.finish() } +/// Crea un archivo temporal para una tabla dada. +/// +/// # Parámetros +/// +/// - `table_name`: El nombre de la tabla. +/// - `table_path`: La ruta del directorio en donde debe estar contenida la tabla. +/// +/// # Retorna +/// +/// Devuelve un `Result` que contiene una tupla con un `File` y un `PathBuf` con la ruta al archivo temporal, o un `Errored` en caso de error. pub fn get_temp_file(table_name: &str, table_path: &Path) -> Result<(File, PathBuf), Errored> { let id = get_temp_id(); let table_path = table_path @@ -60,6 +121,27 @@ pub fn get_temp_file(table_name: &str, table_path: &Path) -> Result<(File, PathB )) } +/// Elimina un archivo temporal, renombrándolo a la ruta de la tabla. +/// +/// # Parámetros +/// +/// - `table_path`: La ruta al archivo de la tabla. +/// - `temp_path`: La ruta al archivo temporal. +/// +/// # Retorna +/// +/// Devuelve un `Result` que indica si la operación tuvo éxito o un `Errored` en caso de error. +/// +/// # Ejemplo +/// +/// ```rust +/// use std::path::Path; +/// use rustic_sql::utils::files::delete_temp_file; +/// +/// let table_path = Path::new("tests/unit_tables/clientes.csv"); +/// let temp_path = Path::new("tests/unit_tables/clientes.csv.tmp"); +/// //delete_temp_file(table_path, temp_path)?; +/// ``` pub fn delete_temp_file(table_path: &Path, temp_path: &Path) -> Result<(), Errored> { if let Some(ex) = temp_path.extension() { if ex.to_string_lossy() != TEMP_EXTENSION { @@ -70,6 +152,16 @@ pub fn delete_temp_file(table_path: &Path, temp_path: &Path) -> Result<(), Error Ok(()) } +/// Asegura que un archivo termine en una nueva línea. +/// +/// # Parámetros +/// +/// - `file`: El archivo que se desea verificar. +/// +/// # Retorna +/// +/// Devuelve un `Result` que indica si la operación tuvo éxito o un `Errored` en caso de error. +/// pub fn make_file_end_in_newline(file: &mut File) -> Result<(), Errored> { file.seek(SeekFrom::End(0))?; if file.metadata()?.len() == 0 { @@ -84,10 +176,30 @@ pub fn make_file_end_in_newline(file: &mut File) -> Result<(), Errored> { Ok(()) } +/// Obtiene un archivo en modo lectura y adición. +/// +/// # Parámetros +/// +/// - `table_path`: La ruta al archivo de la tabla. +/// +/// # Retorna +/// +/// Devuelve un `Result` que contiene un `File` en modo lectura y adición, o un `Errored` en caso de error. +/// pub fn get_table_file(table_path: &Path) -> Result { Ok(File::options().read(true).append(true).open(table_path)?) } +/// Valida si una ruta es un directorio existente y no vacío. +/// +/// # Parámetros +/// +/// - `dir`: La ruta al directorio. +/// +/// # Retorna +/// +/// Devuelve un `Result` que contiene un `&Path` si la validación es exitosa, o un `Errored` en caso de error. +/// pub fn validate_path(dir: &str) -> Result<&Path, Errored> { let path = Path::new(dir); if !path.exists() { From 1a80e135095b3dfc286091c92df8bd5446c217eb Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 08:47:37 -0300 Subject: [PATCH 074/103] fix slight doc issue --- src/utils/files.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/files.rs b/src/utils/files.rs index 73a2a4e..e210aa4 100644 --- a/src/utils/files.rs +++ b/src/utils/files.rs @@ -61,7 +61,6 @@ pub fn split_csv(line: &str) -> Vec { /// # Retorna /// /// Devuelve un `Result` que contiene un `PathBuf` con la ruta al archivo de la tabla si tiene éxito, o un `Errored` en caso de error. -/// ``` pub fn get_table_path(dir_path: &Path, table_name: &str) -> Result { let table_path = dir_path.join(table_name).with_extension(CSV_EXTENSION); if !table_path.is_file() { From dd4325bc4052037e6d77d53ea04d5012e9a76e55 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 08:54:35 -0300 Subject: [PATCH 075/103] readme test --- README.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 75495b5..a1aa6b5 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,23 @@ ![workflow TP](https://github.com/gabokatta/rustic-sql/actions/workflows/rust.yml/badge.svg) -> woooo! +> [!IMPORTANT] +> RUN THE APP + +```BASH +cargo run -- ruta/a/tablas "SELECT * FROM table" > output.csv +``` + +> [!TIP] +> TESTING + +```BASH +cargo test --all +``` + +> [!NOTE] +> DOCUMENTATION + +```BASH +cargo doc --open +``` \ No newline at end of file From 729c41ccedd5a8a6d646655ec2ff654945866eab Mon Sep 17 00:00:00 2001 From: katta <102127372+gabokatta@users.noreply.github.com> Date: Mon, 9 Sep 2024 09:05:22 -0300 Subject: [PATCH 076/103] Update README.md --- README.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index a1aa6b5..f1c2de1 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,24 @@ -# rustic-sql 🦀 - -![workflow TP](https://github.com/gabokatta/rustic-sql/actions/workflows/rust.yml/badge.svg) +# rustic-sql 🦀 ![workflow TP](https://github.com/gabokatta/rustic-sql/actions/workflows/rust.yml/badge.svg) > [!IMPORTANT] -> RUN THE APP - -```BASH -cargo run -- ruta/a/tablas "SELECT * FROM table" > output.csv -``` - +> ¿Como correr la app? +> - Para las queries de tipo SELECT puedes redirigir la salida de STDOUT a un archivo o simplemente ver la salida por pantalla. +> ```BASH +>cargo run -- ruta/a/tablas "SELECT * FROM table" > output.csv +>``` +> - Para las otras queries, simplemente se aplican los cambios sobre los archivos. +> ```BASH +>cargo run -- ruta/a/tablas "INSERT INTO table (id, name) VALUES (1, 'gabriel');" +>``` +___ > [!TIP] -> TESTING - -```BASH -cargo test --all -``` - +> ¿Como testear la app? +>```BASH +>cargo test --all +>``` +___ > [!NOTE] -> DOCUMENTATION - -```BASH -cargo doc --open -``` \ No newline at end of file +> Leer la documentación +>```BASH +>cargo doc --open +>``` From 8048bc47d5f17c31b339401def8b15d97fb10c3c Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 09:14:47 -0300 Subject: [PATCH 077/103] testing doc workflow --- .github/workflows/docs.yml | 66 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .github/workflows/docs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..c13810b --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,66 @@ +name: Docs + +on: + push: + branches: [main] + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: deploy + cancel-in-progress: false + +jobs: + build: + name: Build + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Configure cache + uses: Swatinem/rust-cache@v2 + + - name: Setup pages + id: pages + uses: actions/configure-pages@v4 + + - name: Clean docs folder + run: cargo clean --doc + + - name: Build docs + run: cargo doc --no-deps + + - name: Add redirect + run: echo '' > target/doc/index.html + + - name: Remove lock file + run: rm target/doc/.lock + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./target/doc + + deploy: + name: Deploy + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + runs-on: ubuntu-latest + + needs: build + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file From d75ed5ac98d012dcb7c53a4b3b2ea2b887ec4bdc Mon Sep 17 00:00:00 2001 From: katta <102127372+gabokatta@users.noreply.github.com> Date: Mon, 9 Sep 2024 09:30:05 -0300 Subject: [PATCH 078/103] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f1c2de1..1add8fc 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,4 @@ ___ >```BASH >cargo doc --open >``` +> También se puede ver a través de [Github Pages](https://gabokatta.github.io/rustic-sql/rustic-sql/index.html)! From be71d787514720f64d4afa27dbb539c8d59bcdc5 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 09:37:02 -0300 Subject: [PATCH 079/103] testing doc workflow pt2 --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c13810b..f492041 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -39,7 +39,7 @@ jobs: run: cargo doc --no-deps - name: Add redirect - run: echo '' > target/doc/index.html + run: echo '' > target/doc/index.html - name: Remove lock file run: rm target/doc/.lock From 2d3b856692f6c012608bef04312399ed2df5e394 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 09:45:30 -0300 Subject: [PATCH 080/103] testing doc workflow pt3 --- .github/workflows/docs.yml | 2 +- README.md | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f492041..5a3869d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -39,7 +39,7 @@ jobs: run: cargo doc --no-deps - name: Add redirect - run: echo '' > target/doc/index.html + run: echo '' > /target/doc/rustic_sql/index.html - name: Remove lock file run: rm target/doc/.lock diff --git a/README.md b/README.md index 1add8fc..a16d816 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,7 @@ ___ >``` ___ > [!NOTE] -> Leer la documentación +> Leer la [documentación](https://gabokatta.github.io/rustic-sql/rustic-sql/index.html) >```BASH >cargo doc --open >``` -> También se puede ver a través de [Github Pages](https://gabokatta.github.io/rustic-sql/rustic-sql/index.html)! From d1f8f6b919ac3d6ab10bdb3a59994b7e63f09f46 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 09:50:08 -0300 Subject: [PATCH 081/103] testing doc workflow pt4 --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5a3869d..75bb337 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -39,7 +39,7 @@ jobs: run: cargo doc --no-deps - name: Add redirect - run: echo '' > /target/doc/rustic_sql/index.html + run: echo '' > target/doc/index.html - name: Remove lock file run: rm target/doc/.lock From 48ad5fa57d4c85fb9fda0cb93b1be08b214f8a9d Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 09:52:15 -0300 Subject: [PATCH 082/103] testing doc workflow pt5 --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 75bb337..c13810b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -39,7 +39,7 @@ jobs: run: cargo doc --no-deps - name: Add redirect - run: echo '' > target/doc/index.html + run: echo '' > target/doc/index.html - name: Remove lock file run: rm target/doc/.lock From 447fc1cc78dc19a14254b6c8f8c3fa67660dcb24 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 09:53:45 -0300 Subject: [PATCH 083/103] testing doc workflow pt6 --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c13810b..f492041 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -39,7 +39,7 @@ jobs: run: cargo doc --no-deps - name: Add redirect - run: echo '' > target/doc/index.html + run: echo '' > target/doc/index.html - name: Remove lock file run: rm target/doc/.lock From 2229be62187ef0c12b5cd66142d1c61898da552f Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 09:56:30 -0300 Subject: [PATCH 084/103] testing doc workflow pt7 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a16d816..947da6b 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ ___ >``` ___ > [!NOTE] -> Leer la [documentación](https://gabokatta.github.io/rustic-sql/rustic-sql/index.html) +> Leer la [documentación](https://gabokatta.github.io/rustic-sql/index.html) >```BASH >cargo doc --open >``` From edf1254d45e3c1d307f714918323bc40d1f90309 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 10:02:42 -0300 Subject: [PATCH 085/103] testing doc workflow pt8 --- .github/workflows/docs.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f492041..599e044 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -41,6 +41,12 @@ jobs: - name: Add redirect run: echo '' > target/doc/index.html + - name: Check Dir + run: ls + + - name: Check Dir 2 + run: ls ./target + - name: Remove lock file run: rm target/doc/.lock From cfd342f40402002bd94e357a07d54ab6aabb5229 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 10:05:00 -0300 Subject: [PATCH 086/103] testing doc workflow pt9 --- .github/workflows/docs.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 599e044..e98c9e6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -47,6 +47,9 @@ jobs: - name: Check Dir 2 run: ls ./target + - name: Check Dir 3 + run: ls ./target/doc + - name: Remove lock file run: rm target/doc/.lock From 4f5b604ace54187681636dc068f70629e4c76f58 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 10:09:10 -0300 Subject: [PATCH 087/103] testing doc workflow pt10 --- .github/workflows/docs.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e98c9e6..d1296db 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -42,13 +42,7 @@ jobs: run: echo '' > target/doc/index.html - name: Check Dir - run: ls - - - name: Check Dir 2 - run: ls ./target - - - name: Check Dir 3 - run: ls ./target/doc + run: ls ./target/doc/rustic-sql - name: Remove lock file run: rm target/doc/.lock From 5fe047612943ee712ae28180bd63fff1c2ab3f7f Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 10:10:43 -0300 Subject: [PATCH 088/103] testing doc workflow pt11 --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d1296db..f173b86 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -42,7 +42,7 @@ jobs: run: echo '' > target/doc/index.html - name: Check Dir - run: ls ./target/doc/rustic-sql + run: ls ./target/doc/rustic_sql - name: Remove lock file run: rm target/doc/.lock From 846956718de7e9deaef069b6edb321f289b70b1c Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 10:12:51 -0300 Subject: [PATCH 089/103] testing doc workflow pt12 --- .github/workflows/docs.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f173b86..411cc0a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -39,10 +39,7 @@ jobs: run: cargo doc --no-deps - name: Add redirect - run: echo '' > target/doc/index.html - - - name: Check Dir - run: ls ./target/doc/rustic_sql + run: echo '' > target/doc/rustic_sql/index.html - name: Remove lock file run: rm target/doc/.lock From eac9e9429551ce0d359120b5a04cc8d9b1865651 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 10:17:34 -0300 Subject: [PATCH 090/103] testing doc workflow pt13 --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 411cc0a..8501bb3 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -39,7 +39,7 @@ jobs: run: cargo doc --no-deps - name: Add redirect - run: echo '' > target/doc/rustic_sql/index.html + run: echo '' > target/doc/rustic_sql/index.html - name: Remove lock file run: rm target/doc/.lock From db6368a0a71db6e1dfd83697d7d2c55924e50069 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 10:24:55 -0300 Subject: [PATCH 091/103] return workflow to default --- .github/workflows/docs.yml | 130 ++++++++++++++++++------------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8501bb3..9fe08ac 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,66 +1,66 @@ -name: Docs - -on: - push: - branches: [main] - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: deploy - cancel-in-progress: false - -jobs: - build: - name: Build - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Rust - uses: dtolnay/rust-toolchain@stable - - - name: Configure cache - uses: Swatinem/rust-cache@v2 - - - name: Setup pages - id: pages - uses: actions/configure-pages@v4 - - - name: Clean docs folder - run: cargo clean --doc - - - name: Build docs - run: cargo doc --no-deps - - - name: Add redirect - run: echo '' > target/doc/rustic_sql/index.html - - - name: Remove lock file - run: rm target/doc/.lock - - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: ./target/doc - - deploy: - name: Deploy - - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - - runs-on: ubuntu-latest - - needs: build - - steps: - - name: Deploy to GitHub Pages - id: deployment +name: Docs + +on: + push: + branches: [main] + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: deploy + cancel-in-progress: false + +jobs: + build: + name: Build + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Configure cache + uses: Swatinem/rust-cache@v2 + + - name: Setup pages + id: pages + uses: actions/configure-pages@v4 + + - name: Clean docs folder + run: cargo clean --doc + + - name: Build docs + run: cargo doc --no-deps + + - name: Add redirect + run: echo '' > target/doc/index.html + + - name: Remove lock file + run: rm target/doc/.lock + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./target/doc + + deploy: + name: Deploy + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + runs-on: ubuntu-latest + + needs: build + + steps: + - name: Deploy to GitHub Pages + id: deployment uses: actions/deploy-pages@v4 \ No newline at end of file From b6b3510bf18a5ef52d977a99800889188bb03cd8 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 10:29:43 -0300 Subject: [PATCH 092/103] removed redirect from workflow --- .github/workflows/docs.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9fe08ac..425a227 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -38,9 +38,6 @@ jobs: - name: Build docs run: cargo doc --no-deps - - name: Add redirect - run: echo '' > target/doc/index.html - - name: Remove lock file run: rm target/doc/.lock From 3bc726704f5b7081c99840cfd2d95c50611cf85b Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 10:45:32 -0300 Subject: [PATCH 093/103] snake case fix, maybe? --- .github/workflows/docs.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 425a227..4644727 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -38,6 +38,9 @@ jobs: - name: Build docs run: cargo doc --no-deps + - name: Add redirect + run: echo '' > target/doc/index.html + - name: Remove lock file run: rm target/doc/.lock From caf736c42044bfe68e4b1a8ee99762069e89187f Mon Sep 17 00:00:00 2001 From: katta <102127372+gabokatta@users.noreply.github.com> Date: Mon, 9 Sep 2024 10:49:11 -0300 Subject: [PATCH 094/103] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 947da6b..9846f89 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ ___ >``` ___ > [!NOTE] -> Leer la [documentación](https://gabokatta.github.io/rustic-sql/index.html) +> ¿Como levantar la documentación? >```BASH >cargo doc --open >``` +> Alternativamente se puede visualizar por [github!](https://gabokatta.github.io/rustic-sql) From 52ac61691dc461f873a9693769c1789a1694cbc3 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 11:17:27 -0300 Subject: [PATCH 095/103] final doc update --- src/lib.rs | 53 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index fe94fb1..8b75a6f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,40 @@ +//! # RusticSQL 🦀 +//! +//! ### Clon de SQL, en Rust. +//! +//! Permite al usuario ejecutar consultas SQL mediante la linea de comandos. +//! +//! Las operaciones se realizan sobre "tablas" (archivos csv). +//! +//! +//! Consultas Permitidas: [SELECT, INSERT, UPDATE, DELETE] +//! +//! Operadores Disponibles: [AND, OR, NOT y comparadores simples (>, <, =, etc..)] +//! +//! +//! Estructura del Proyecto: +//! - Tokenizador: Recibe un String y te devuelve tokens. +//! - Constructor: Recibe tokens y los transforma en consultas validas. +//! - Ejecutor: Recibe consultas y las ejecuta sobre las tablas. +//! +//! # Usar RusticSQL: +//! +//! > ```BASH +//! > cargo run -- ruta/a/tablas "SELECT * FROM table" > output.csv +//! > ``` +//! +//! # Testea RusticSQL: +//! +//! >```BASH +//! >cargo test --all +//! >``` +//! +//! +#![doc( + html_logo_url = "https://cdn-icons-png.flaticon.com/512/4726/4726022.png", + html_favicon_url = "https://cdn-icons-png.flaticon.com/512/4726/4726022.png" +)] + use crate::query::executor::Executor; use crate::query::structs::query::Query; use crate::query::tokenizer::Tokenizer; @@ -8,22 +45,6 @@ use std::error::Error; pub mod query; pub mod utils; -/// Ejecuta la aplicación RusticSQL a partir de los argumentos proporcionados. -/// -/// Esta función valida los argumentos de la línea de comandos, procesa la consulta SQL y ejecuta -/// la consulta en los datos especificados. -/// -/// # Argumentos -/// -/// - `args`: Un vector de `String` que contiene los argumentos de la línea de comandos. Se espera que -/// contenga exactamente tres elementos: el nombre del comando, la ruta a las tablas y la consulta SQL. -/// -/// # Errores -/// -/// - Retorna un error si el número de argumentos es incorrecto. -/// - Retorna un error si la ruta a las tablas no es válida. -/// - Retorna un error si la consulta SQL no es válida. -/// - Retorna un error si el procesamiento de la consulta o la ejecución falla. pub fn run(args: Vec) -> Result<(), Box> { if args.len() != 3 { println!("invalid usage of rustic-sql"); From cc2be7602263d4db8b0ed40578b680ddd80f3c3e Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 11:21:12 -0300 Subject: [PATCH 096/103] document private items (educational-purposes) --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4644727..2a5ee14 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -36,7 +36,7 @@ jobs: run: cargo clean --doc - name: Build docs - run: cargo doc --no-deps + run: cargo doc --document-private-items --no-deps - name: Add redirect run: echo '' > target/doc/index.html From f759d6a00544499c9f496054d36cf530fbe6a5d4 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 11:26:02 -0300 Subject: [PATCH 097/103] clippy --- src/utils/files.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/files.rs b/src/utils/files.rs index e210aa4..3380612 100644 --- a/src/utils/files.rs +++ b/src/utils/files.rs @@ -170,7 +170,7 @@ pub fn make_file_end_in_newline(file: &mut File) -> Result<(), Errored> { file.seek(SeekFrom::End(-1))?; file.read_exact(&mut last_byte)?; if last_byte[0] != b'\n' { - file.write_all(&[b'\n'])?; + file.write_all(b"\n")?; } Ok(()) } From c81ca88c81a5a084b05b9ea831d4c62c77747121 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 11:52:51 -0300 Subject: [PATCH 098/103] fix select projection bug --- src/query/executor/select.rs | 20 ++++++++++++++++---- src/query/structs/row.rs | 7 +++---- tests/select.rs | 6 ++++++ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/query/executor/select.rs b/src/query/executor/select.rs index 7e7b52d..25c6200 100644 --- a/src/query/executor/select.rs +++ b/src/query/executor/select.rs @@ -43,8 +43,7 @@ impl Executor { } } self.sort_rows(&mut matched_rows, &header)?; - println!("{}", header.join(",")); - self.output_rows(&matched_rows); + self.output_rows(&header, &matched_rows); Ok(()) } @@ -95,9 +94,22 @@ impl Executor { /// # Ejemplo /// /// Este método es llamado internamente por `run_select`, por lo que no tiene un ejemplo de uso independiente. - fn output_rows(&self, matched_rows: &[Row]) { + fn output_rows(&self, header: &[String], matched_rows: &[Row]) { + let mut values = vec![]; + if self.query.columns.is_empty() { + println!("{}", header.join(",")); + } + else { + values = self + .query + .columns + .iter() + .map(|t| t.value.to_string()) + .collect(); + println!("{}", values.join(",")); + } for row in matched_rows { - row.print_projection(&self.query.columns) + row.print_projection(&values) } } diff --git a/src/query/structs/row.rs b/src/query/structs/row.rs index f6ebe6e..5b1a3d5 100644 --- a/src/query/structs/row.rs +++ b/src/query/structs/row.rs @@ -258,14 +258,13 @@ impl<'a> Row<'a> { /// let mut row = Row::new(&header); /// row.set("id", "360".to_string()).unwrap(); /// row.set("apellido", "katta".to_string()).unwrap(); - /// row.print_projection(&vec![Token { kind: Identifier, value: "id".to_string() }]); + /// row.print_projection(&vec!["id".to_string()]); /// ``` - pub fn print_projection(&self, columns: &[Token]) { + pub fn print_projection(&self, columns: &Vec) { if columns.is_empty() { println!("{}", self.as_csv_row()); } else { - let values: Vec = columns.iter().map(|t| t.value.to_string()).collect(); - println!("{}", self.as_csv_projection(&values)); + println!("{}", self.as_csv_projection(columns)); } } diff --git a/tests/select.rs b/tests/select.rs index 559cf17..e910ee4 100644 --- a/tests/select.rs +++ b/tests/select.rs @@ -22,6 +22,7 @@ fn test_select_no_where() { fn test_select_where_with_order_by() { let test = RusticSQLTest::default(); let query = "SELECT name, email FROM users WHERE age > 30 ORDER BY age DESC"; + let expected_rows: Vec = [ vec!["Bob Brown", "bob.brown@example.com"], vec!["Frank Miller", "frank.miller@example.com"], @@ -33,7 +34,10 @@ fn test_select_where_with_order_by() { .iter() .map(|r| r.join(",")) .collect(); + let expected_header: String = ["name", "email"].join(","); let result = test.run_and_get_rows(query.to_string()); + + assert_eq!(expected_header, result[0]); assert_eq!(expected_rows, result[1..]); } @@ -52,8 +56,10 @@ fn test_select_with_nested_where() { .iter() .map(|r| r.join(",")) .collect(); + let expected_header: String = ["user_id", "name"].join(","); let result = test.run_and_get_rows(query.to_string()); + assert_eq!(expected_header, result[0]); assert_eq!(expected_rows, result[1..]); } From bc078031e0d87bb110aa620a6a2ab7affbb2ea28 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 11:53:07 -0300 Subject: [PATCH 099/103] format --- src/query/executor/select.rs | 3 +-- tests/select.rs | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/query/executor/select.rs b/src/query/executor/select.rs index 25c6200..e43cb1b 100644 --- a/src/query/executor/select.rs +++ b/src/query/executor/select.rs @@ -98,8 +98,7 @@ impl Executor { let mut values = vec![]; if self.query.columns.is_empty() { println!("{}", header.join(",")); - } - else { + } else { values = self .query .columns diff --git a/tests/select.rs b/tests/select.rs index e910ee4..5994599 100644 --- a/tests/select.rs +++ b/tests/select.rs @@ -22,7 +22,7 @@ fn test_select_no_where() { fn test_select_where_with_order_by() { let test = RusticSQLTest::default(); let query = "SELECT name, email FROM users WHERE age > 30 ORDER BY age DESC"; - + let expected_rows: Vec = [ vec!["Bob Brown", "bob.brown@example.com"], vec!["Frank Miller", "frank.miller@example.com"], @@ -36,7 +36,7 @@ fn test_select_where_with_order_by() { .collect(); let expected_header: String = ["name", "email"].join(","); let result = test.run_and_get_rows(query.to_string()); - + assert_eq!(expected_header, result[0]); assert_eq!(expected_rows, result[1..]); } From 5a239cafd08aab40aa2051844fac0f8dd4c2e2d9 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 12:09:30 -0300 Subject: [PATCH 100/103] new tests and fixed bug from projection --- src/query/builder/mod.rs | 3 +++ src/query/executor/select.rs | 12 ++++++++---- tests/insert.rs | 9 +++++++++ tests/select.rs | 8 ++++++++ 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/query/builder/mod.rs b/src/query/builder/mod.rs index 0973f19..56775c2 100644 --- a/src/query/builder/mod.rs +++ b/src/query/builder/mod.rs @@ -94,6 +94,9 @@ pub trait Builder { } } Keyword if t.value == "FROM" || t.value == "VALUES" => { + if fields.is_empty() { + errored!(Syntax, "read FROM without any * or fields in query.") + } break; } ParenthesisClose => { diff --git a/src/query/executor/select.rs b/src/query/executor/select.rs index e43cb1b..30a0910 100644 --- a/src/query/executor/select.rs +++ b/src/query/executor/select.rs @@ -91,24 +91,28 @@ impl Executor { /// Este método toma las filas coincidentes y las imprime en la salida estándar, proyectando solo las columnas /// especificadas en la consulta SQL (`self.query.columns`). /// + /// Ademas, se encarga de imprimir la proyección del header del csv. + /// Si las columnas proyectadas son vacias, se asume que el operador * esta siendo usado, + /// de lo contrario se imprime el header proyectado a las columnas. + /// /// # Ejemplo /// /// Este método es llamado internamente por `run_select`, por lo que no tiene un ejemplo de uso independiente. fn output_rows(&self, header: &[String], matched_rows: &[Row]) { - let mut values = vec![]; + let mut columns = vec![]; if self.query.columns.is_empty() { println!("{}", header.join(",")); } else { - values = self + columns = self .query .columns .iter() .map(|t| t.value.to_string()) .collect(); - println!("{}", values.join(",")); + println!("{}", columns.join(",")); } for row in matched_rows { - row.print_projection(&values) + row.print_projection(&columns) } } diff --git a/tests/insert.rs b/tests/insert.rs index 2e7b3e0..3b76e66 100644 --- a/tests/insert.rs +++ b/tests/insert.rs @@ -15,6 +15,15 @@ fn test_insert_user_all_fields() { ) } +#[test] +fn test_insert_nothing() { + let test = RusticSQLTest::default(); + let query = "INSERT INTO users VALUES (14, 'Solidus Snake', 'solidus.snake@mgs.com', 40)"; + let result = test.run_for(query.to_string()); + print!("{:?}", &result); + assert!(result.is_err()); +} + #[test] fn test_insert_multiple_rows() { let test = RusticSQLTest::default(); diff --git a/tests/select.rs b/tests/select.rs index 5994599..91375a3 100644 --- a/tests/select.rs +++ b/tests/select.rs @@ -63,6 +63,14 @@ fn test_select_with_nested_where() { assert_eq!(expected_rows, result[1..]); } +#[test] +fn test_select_with_no_fields() { + let test = RusticSQLTest::default(); + let query = "SELECT FROM users ORDER BY id WHERE 1=1"; + let result = test.run_for(query.to_string()); + assert!(result.is_err()) +} + #[test] fn test_select_with_invalid_order_field() { let test = RusticSQLTest::default(); From a491a1b1a114d5bd6b3a7eb9ea613a5f080411b7 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 12:10:33 -0300 Subject: [PATCH 101/103] renamed output_rows into output_projection --- src/query/executor/select.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/query/executor/select.rs b/src/query/executor/select.rs index 30a0910..8cbd891 100644 --- a/src/query/executor/select.rs +++ b/src/query/executor/select.rs @@ -43,7 +43,7 @@ impl Executor { } } self.sort_rows(&mut matched_rows, &header)?; - self.output_rows(&header, &matched_rows); + self.output_projection(&header, &matched_rows); Ok(()) } @@ -98,7 +98,7 @@ impl Executor { /// # Ejemplo /// /// Este método es llamado internamente por `run_select`, por lo que no tiene un ejemplo de uso independiente. - fn output_rows(&self, header: &[String], matched_rows: &[Row]) { + fn output_projection(&self, header: &[String], matched_rows: &[Row]) { let mut columns = vec![]; if self.query.columns.is_empty() { println!("{}", header.join(",")); From d346878f12fe6686cfab168dab885c1186df24e9 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 13:26:19 -0300 Subject: [PATCH 102/103] did some more testing, found some bugs with ordering --- src/query/executor/select.rs | 18 ++- tests/integration_tables/people.csv | 201 ++++++++++++++++++++++++++++ 2 files changed, 214 insertions(+), 5 deletions(-) create mode 100644 tests/integration_tables/people.csv diff --git a/src/query/executor/select.rs b/src/query/executor/select.rs index 8cbd891..83a8cb0 100644 --- a/src/query/executor/select.rs +++ b/src/query/executor/select.rs @@ -53,6 +53,9 @@ impl Executor { /// especificados en `self.query.ordering`. Verifica si los campos de ordenamiento existen en el encabezado /// de la tabla y realiza la comparación correspondiente. /// + /// Si hay varios ordenamientos en la consulta, primero se evalua uno y si el resultado es igual, + /// se compara por el siguiente. + /// /// # Errores /// /// Retorna un error si alguno de los campos de ordenamiento no existe en el encabezado. @@ -69,20 +72,25 @@ impl Executor { &order.field.value ) } - matched_rows.sort_by(|a, b| { + } + matched_rows.sort_by(|a, b| { + for order in &self.query.ordering { let l = ExpressionNode::get_variable_value(&a.values, &order.field); let r = ExpressionNode::get_variable_value(&b.values, &order.field); if let (Ok(a), Ok(b)) = (l, r) { - return match order.kind { + let comparison_result = match order.kind { OrderKind::Asc => ExpressionComparator::compare_ordering(&a, &b) .unwrap_or(Ordering::Equal), OrderKind::Desc => ExpressionComparator::compare_ordering(&b, &a) .unwrap_or(Ordering::Equal), }; + if comparison_result != Ordering::Equal { + return comparison_result; + } } - Ordering::Equal - }) - } + } + Ordering::Equal + }); Ok(()) } diff --git a/tests/integration_tables/people.csv b/tests/integration_tables/people.csv new file mode 100644 index 0000000..896ce51 --- /dev/null +++ b/tests/integration_tables/people.csv @@ -0,0 +1,201 @@ +id,last_name,email,age,gender +1,Labin,llabin0@nps.gov,58,0 +2,Soppeth,jsoppeth1@chron.com,50,1 +3,Godson,dgodson2@parallels.com,29,1 +4,Drioli,kdrioli3@over-blog.com,56,1 +5,Fairley,jfairley4@skyrock.com,24,1 +6,Chittleburgh,uchittleburgh5@cdc.gov,27,0 +7,Daine,wdaine6@blinklist.com,35,0 +8,Sommerville,dsommerville7@addtoany.com,39,1 +9,Leavesley,nleavesley8@netvibes.com,31,1 +10,Beidebeke,cbeidebeke9@naver.com,53,1 +11,Czyz,aczyza@issuu.com,36,0 +12,Howle,thowleb@mtv.com,24,1 +13,Haggerty,shaggertyc@youtu.be,20,1 +14,Gallatly,ngallatlyd@marketwatch.com,51,0 +15,Brinkman,cbrinkmane@geocities.com,28,0 +16,Willcocks,bwillcocksf@xing.com,48,0 +17,Gurr,kgurrg@cisco.com,36,0 +18,Calow,fcalowh@wix.com,48,1 +19,Forsyde,dforsydei@apple.com,31,1 +20,Fieldgate,ffieldgatej@list-manage.com,51,1 +21,Hargitt,dhargittk@nyu.edu,49,0 +22,Tritton,dtrittonl@unc.edu,21,1 +23,Dufaire,sdufairem@rakuten.co.jp,51,0 +24,Cawcutt,bcawcuttn@answers.com,54,0 +25,Barczynski,lbarczynskio@tmall.com,59,1 +26,Daybell,mdaybellp@wufoo.com,22,1 +27,Comins,mcominsq@lycos.com,44,0 +28,Mowle,gmowler@marriott.com,46,1 +29,King,akings@squarespace.com,41,1 +30,Ziemecki,mziemeckit@usatoday.com,41,0 +31,De Atta,pdeattau@oracle.com,57,0 +32,Kewish,bkewishv@photobucket.com,46,0 +33,Banck,abanckw@surveymonkey.com,43,1 +34,Inglesant,hinglesantx@nyu.edu,28,0 +35,Probart,dprobarty@gmpg.org,39,1 +36,Pfeffel,jpfeffelz@mlb.com,52,1 +37,MacClancey,cmacclancey10@opera.com,51,1 +38,Evensden,mevensden11@opera.com,51,1 +39,De Paepe,cdepaepe12@w3.org,27,0 +40,Guillou,jguillou13@flickr.com,29,0 +41,Trevan,ktrevan14@comcast.net,39,1 +42,Polamontayne,ipolamontayne15@baidu.com,51,0 +43,Chaffer,echaffer16@desdev.cn,23,1 +44,Buist,sbuist17@dot.gov,22,1 +45,Gravell,agravell18@oakley.com,20,1 +46,Penton,ipenton19@skype.com,50,1 +47,Vaar,gvaar1a@rakuten.co.jp,23,0 +48,Scorer,sscorer1b@npr.org,44,0 +49,Imlock,timlock1c@flavors.me,22,0 +50,Baldcock,ebaldcock1d@amazon.co.jp,48,1 +51,Joselevitch,ajoselevitch1e@youku.com,22,0 +52,Gipps,egipps1f@prnewswire.com,53,0 +53,Heeran,mheeran1g@columbia.edu,58,0 +54,Collet,ecollet1h@sfgate.com,39,1 +55,Serjent,oserjent1i@hao123.com,49,1 +56,Bradberry,jbradberry1j@apple.com,57,0 +57,Coysh,xcoysh1k@pbs.org,26,1 +58,Winston,mwinston1l@newyorker.com,47,0 +59,Tinman,ctinman1m@sphinn.com,55,1 +60,Widdecombe,hwiddecombe1n@delicious.com,35,1 +61,Vernay,bvernay1o@admin.ch,59,1 +62,Gallop,pgallop1p@answers.com,47,1 +63,Krystof,kkrystof1q@statcounter.com,52,1 +64,Gardiner,ggardiner1r@huffingtonpost.com,58,0 +65,Issitt,vissitt1s@spotify.com,52,1 +66,D'Alwis,pdalwis1t@nyu.edu,20,0 +67,Cargenven,ccargenven1u@moonfruit.com,28,1 +68,Smitheram,tsmitheram1v@blinklist.com,38,1 +69,Apfelmann,eapfelmann1w@icq.com,55,0 +70,Rasher,crasher1x@amazonaws.com,28,0 +71,Eathorne,neathorne1y@miibeian.gov.cn,55,0 +72,Buddle,kbuddle1z@linkedin.com,47,0 +73,Kolak,ckolak20@i2i.jp,35,1 +74,Brimble,mbrimble21@taobao.com,20,1 +75,Dublin,rdublin22@storify.com,20,0 +76,Tudor,mtudor23@g.co,24,0 +77,Barkas,hbarkas24@ustream.tv,36,1 +78,Chessor,nchessor25@icio.us,29,1 +79,Balnave,mbalnave26@unblog.fr,20,0 +80,Hutchin,jhutchin27@japanpost.jp,60,1 +81,Crosfield,bcrosfield28@nsw.gov.au,38,1 +82,Jeanon,rjeanon29@macromedia.com,34,0 +83,Lowrie,clowrie2a@foxnews.com,22,0 +84,Kollach,akollach2b@cdc.gov,56,0 +85,MacCrossan,rmaccrossan2c@sogou.com,60,1 +86,Snelgrove,asnelgrove2d@mysql.com,55,0 +87,Will,ywill2e@phpbb.com,21,0 +88,Tefft,dtefft2f@blogger.com,58,1 +89,Broadbent,mbroadbent2g@bigcartel.com,33,1 +90,Crosswaite,ecrosswaite2h@yahoo.com,26,1 +91,Stickings,lstickings2i@bluehost.com,56,0 +92,Lightwing,elightwing2j@nps.gov,44,1 +93,Matuschek,amatuschek2k@mediafire.com,32,0 +94,Dreinan,mdreinan2l@bbb.org,60,0 +95,Weinham,tweinham2m@comsenz.com,38,0 +96,O'Hear,sohear2n@163.com,52,1 +97,Ledwidge,aledwidge2o@51.la,49,1 +98,Lumsdale,slumsdale2p@usda.gov,24,0 +99,Antonin,gantonin2q@infoseek.co.jp,51,1 +100,Terram,hterram2r@instagram.com,56,1 +101,Golt,mgolt2s@sbwire.com,45,1 +102,Nast,knast2t@com.com,59,1 +103,Macveigh,smacveigh2u@liveinternet.ru,29,1 +104,Cottier,bcottier2v@webnode.com,42,0 +105,Hawkslee,phawkslee2w@dedecms.com,58,0 +106,Ladley,aladley2x@fema.gov,41,1 +107,Blum,ablum2y@sakura.ne.jp,40,0 +108,Quartley,kquartley2z@sogou.com,23,0 +109,Nurden,cnurden30@devhub.com,39,0 +110,Franzetti,afranzetti31@lycos.com,34,0 +111,Turfrey,iturfrey32@netvibes.com,42,0 +112,Caldero,bcaldero33@examiner.com,21,0 +113,Yoskowitz,jyoskowitz34@wisc.edu,39,1 +114,Vallentin,bvallentin35@ted.com,47,1 +115,Sked,rsked36@sun.com,33,0 +116,Nuschke,dnuschke37@omniture.com,26,0 +117,Ghelardi,eghelardi38@liveinternet.ru,39,1 +118,Barniss,abarniss39@cbc.ca,31,0 +119,Robiot,trobiot3a@chron.com,50,0 +120,Bracken,dbracken3b@mozilla.com,32,1 +121,Gareisr,bgareisr3c@cargocollective.com,21,1 +122,McSaul,mmcsaul3d@myspace.com,45,0 +123,Maciaszek,pmaciaszek3e@hibu.com,34,1 +124,Elson,oelson3f@plala.or.jp,59,0 +125,Criple,ncriple3g@livejournal.com,29,0 +126,Kenan,tkenan3h@engadget.com,24,1 +127,Kneeshaw,lkneeshaw3i@yahoo.co.jp,52,0 +128,Critchlow,lcritchlow3j@ask.com,58,0 +129,Pitman,gpitman3k@wordpress.org,55,1 +130,O'Hagerty,dohagerty3l@cnn.com,34,1 +131,Vouls,jvouls3m@meetup.com,59,1 +132,Catto,gcatto3n@walmart.com,55,1 +133,Hackinge,fhackinge3o@about.com,39,1 +134,Llop,hllop3p@histats.com,39,0 +135,Derrell,cderrell3q@mit.edu,57,0 +136,Geare,ggeare3r@canalblog.com,52,1 +137,Downing,ndowning3s@time.com,23,1 +138,Gilhooly,egilhooly3t@chron.com,60,1 +139,Branchet,rbranchet3u@google.com.au,50,0 +140,Bartaloni,gbartaloni3v@wikimedia.org,25,0 +141,Winslett,vwinslett3w@about.com,51,1 +142,Cantillon,ccantillon3x@boston.com,44,1 +143,Petrulis,bpetrulis3y@baidu.com,34,0 +144,Belfelt,kbelfelt3z@prnewswire.com,46,0 +145,Liggons,yliggons40@phpbb.com,57,0 +146,Perllman,dperllman41@vimeo.com,31,1 +147,Fortescue,jfortescue42@moonfruit.com,47,1 +148,Vannoort,dvannoort43@prweb.com,47,0 +149,Haggus,khaggus44@studiopress.com,22,0 +150,Matic,bmatic45@miibeian.gov.cn,34,1 +151,Oak,soak46@nsw.gov.au,49,1 +152,Huntly,khuntly47@baidu.com,33,0 +153,Verner,dverner48@engadget.com,38,1 +154,Durn,cdurn49@vinaora.com,42,1 +155,Kalisch,ckalisch4a@cloudflare.com,60,1 +156,Meert,zmeert4b@shop-pro.jp,24,1 +157,Ilbert,cilbert4c@theatlantic.com,36,1 +158,Cariss,dcariss4d@sina.com.cn,31,1 +159,Conti,dconti4e@plala.or.jp,47,0 +160,Fewings,afewings4f@google.cn,56,1 +161,Dickinson,ldickinson4g@ox.ac.uk,30,1 +162,Cleare,mcleare4h@ft.com,52,1 +163,Sunley,csunley4i@furl.net,22,1 +164,Legier,blegier4j@people.com.cn,44,0 +165,Stiegars,cstiegars4k@hubpages.com,48,0 +166,Dripp,ndripp4l@scribd.com,57,1 +167,Johl,ejohl4m@pen.io,45,1 +168,Bardnam,abardnam4n@1688.com,42,1 +169,Carff,dcarff4o@etsy.com,48,0 +170,Giannoni,jgiannoni4p@domainmarket.com,54,1 +171,Halpeine,ohalpeine4q@merriam-webster.com,26,1 +172,Swindlehurst,cswindlehurst4r@cisco.com,30,0 +173,Edger,pedger4s@columbia.edu,49,0 +174,Covey,lcovey4t@ycombinator.com,55,0 +175,Ilyasov,jilyasov4u@cbslocal.com,54,1 +176,Sangster,msangster4v@paypal.com,48,1 +177,Blint,nblint4w@delicious.com,60,0 +178,Bowden,mbowden4x@nhs.uk,21,1 +179,McPhilemy,wmcphilemy4y@whitehouse.gov,50,0 +180,Rannie,rrannie4z@weather.com,25,0 +181,O'Sherin,cosherin50@friendfeed.com,24,0 +182,Symes,tsymes51@usda.gov,52,0 +183,Rubra,wrubra52@paginegialle.it,41,0 +184,Lindblad,ilindblad53@acquirethisname.com,24,1 +185,Easterby,beasterby54@cargocollective.com,59,0 +186,Matten,cmatten55@abc.net.au,57,1 +187,Waskett,hwaskett56@noaa.gov,21,1 +188,Siemons,dsiemons57@salon.com,24,1 +189,Terrington,jterrington58@addthis.com,23,1 +190,Calender,mcalender59@facebook.com,50,0 +191,Mullaney,cmullaney5a@virginia.edu,32,0 +192,Beardshaw,pbeardshaw5b@google.com,43,1 +193,Bogey,hbogey5c@bbb.org,43,1 +194,Shannahan,mshannahan5d@dedecms.com,49,1 +195,Mulvaney,lmulvaney5e@fema.gov,36,0 +196,Greenhow,jgreenhow5f@1688.com,21,1 +197,Battelle,lbattelle5g@baidu.com,54,0 +198,Lippett,tlippett5h@spotify.com,45,1 +199,Whoston,swhoston5i@addtoany.com,33,0 +200,Oxshott,joxshott5j@histats.com,30,1 From 435ce2b95a4d927b30aaa8cfd5352386b09da96d Mon Sep 17 00:00:00 2001 From: gabokatta Date: Mon, 9 Sep 2024 13:32:18 -0300 Subject: [PATCH 103/103] new select test --- tests/select.rs | 16 ++++++++++++++++ tests/utils/mod.rs | 3 +++ 2 files changed, 19 insertions(+) diff --git a/tests/select.rs b/tests/select.rs index 91375a3..1277e41 100644 --- a/tests/select.rs +++ b/tests/select.rs @@ -41,6 +41,22 @@ fn test_select_where_with_order_by() { assert_eq!(expected_rows, result[1..]); } +#[test] +fn test_select_where_with_multiple_order_by() { + let test = RusticSQLTest::default(); + let query = + "SELECT id FROM people WHERE age < 22 and gender = 0 ORDER BY last_name DESC, email"; + let expected_rows: Vec = [vec!["87"], vec!["75"], vec!["66"], vec!["112"], vec!["79"]] + .iter() + .map(|r| r.join(",")) + .collect(); + let expected_header: String = ["id"].join(","); + let result = test.run_and_get_rows(query.to_string()); + + assert_eq!(expected_header, result[0]); + assert_eq!(expected_rows, result[1..]); +} + #[test] fn test_select_with_nested_where() { let test = RusticSQLTest::default(); diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs index 110c2d7..ce85120 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -86,12 +86,15 @@ impl Default for RusticSQLTest { let og_tables_path = Path::new("./tests/integration_tables"); let pokemons = og_tables_path.join("pokemon.csv"); let users = og_tables_path.join("users.csv"); + let people = og_tables_path.join("people.csv"); let temp_orders = temp_dir.join("pokemon.csv"); let temp_users = temp_dir.join("users.csv"); + let temp_people = temp_dir.join("people.csv"); fs::copy(pokemons, &temp_orders).expect("failed to copy order table."); fs::copy(users, &temp_users).expect("failed to copy user table."); + fs::copy(people, &temp_people).expect("failed to copy people table."); RusticSQLTest { temp_dir: temp_dir.to_path_buf(),