diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..2a5ee14 --- /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 --document-private-items --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 diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..c8292ff --- /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: 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 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a8cabc --- /dev/null +++ b/.gitignore @@ -0,0 +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/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3ea88fa --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "rustic-sql" +version = "0.1.0" +edition = "2021" diff --git a/README.md b/README.md new file mode 100644 index 0000000..9846f89 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# rustic-sql 🦀 ![workflow TP](https://github.com/gabokatta/rustic-sql/actions/workflows/rust.yml/badge.svg) + +> [!IMPORTANT] +> ¿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] +> ¿Como testear la app? +>```BASH +>cargo test --all +>``` +___ +> [!NOTE] +> ¿Como levantar la documentación? +>```BASH +>cargo doc --open +>``` +> Alternativamente se puede visualizar por [github!](https://gabokatta.github.io/rustic-sql) diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8b75a6f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,63 @@ +//! # 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; +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)?; + Executor::run(path, query)?; + Ok(()) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a343add --- /dev/null +++ b/src/main.rs @@ -0,0 +1,9 @@ +use rustic_sql::run; +use std::env; + +fn main() { + let args = env::args().collect(); + if let Err(e) = run(args) { + println!("{}", e); + } +} diff --git a/src/query/builder/delete.rs b/src/query/builder/delete.rs new file mode 100644 index 0000000..a431409 --- /dev/null +++ b/src/query/builder/delete.rs @@ -0,0 +1,146 @@ +use crate::query::builder::{validate_keywords, Builder}; +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"]; + +/// 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()?; + query.operation = Delete; + query.table = self.parse_table(Delete)?; + match self.peek_expecting("WHERE", Keyword) { + Ok(_) => { + query.conditions = self.parse_where()?; + } + Err(_) => self.expect_none()?, + } + 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) + } +} + +#[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; + use crate::query::tokenizer::Tokenizer; + + fn tokenize(sql: &str) -> Vec { + let mut tokenizer = Tokenizer::new(); + tokenizer.tokenize(sql).unwrap() + } + + #[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 new file mode 100644 index 0000000..d4e2896 --- /dev/null +++ b/src/query/builder/expression.rs @@ -0,0 +1,400 @@ +use crate::errored; +use crate::query::structs::expression::ExpressionNode::{Empty, Leaf}; +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; + +/// 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() { + if t.kind != Keyword || t.value != "OR" { + break; + } + tokens.pop_front(); + let right = ExpressionBuilder::parse_and(tokens)?; + left = ExpressionNode::Statement { + operator: Or, + left: Box::new(left), + right: Box::new(right), + }; + } + 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() { + if t.kind != Keyword || t.value != "AND" { + break; + } + tokens.pop_front(); + let right = ExpressionBuilder::parse_not(tokens)?; + left = ExpressionNode::Statement { + operator: And, + left: Box::new(left), + right: Box::new(right), + }; + } + 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" { + tokens.pop_front(); + let node = ExpressionBuilder::parse_comparisons(tokens)?; + return Ok(ExpressionNode::Statement { + operator: Not, + left: Box::new(node), + right: Box::new(Empty), + }); + } + } + 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); + 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), + }) + } + + /// 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() { + 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), + } + } + 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() + .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) + } +} + +#[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 new file mode 100644 index 0000000..63f0717 --- /dev/null +++ b/src/query/builder/insert.rs @@ -0,0 +1,228 @@ +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"]; + +/// 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)?; + let mut inserts = vec![]; + let mut values = vec![]; + while let Some(t) = self.tokens.front() { + match t.kind { + TokenKind::String | TokenKind::Number => { + if let Some(token) = self.tokens.pop_front() { + values.push(token); + } + } + ParenthesisOpen => { + self.tokens.pop_front(); + } + ParenthesisClose => { + self.tokens.pop_front(); + inserts.push(values); + values = vec![]; + } + _ => unexpected_token_in_stage("VALUES", t)?, + } + } + 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(); + 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(()) + } +} + +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()?; + query.operation = Insert; + query.table = self.parse_table(Insert)?; + self.peek_expecting("(", ParenthesisOpen)?; + query.columns = self.parse_columns()?; + query.inserts = self.parse_insert_values()?; + self.expect_none()?; + self.validate_inserts(&query)?; + 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) + } +} + +#[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/mod.rs b/src/query/builder/mod.rs new file mode 100644 index 0000000..56775c2 --- /dev/null +++ b/src/query/builder/mod.rs @@ -0,0 +1,272 @@ +pub mod delete; +pub mod expression; +pub mod insert; +pub mod select; +pub mod update; + +use crate::errored; +use crate::query::builder::expression::ExpressionBuilder; +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 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 => { + self.peek_expecting("FROM", Keyword)?; + self.tokens().pop_front(); + } + _ => {} + } + 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) + } + + /// 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() { + match t.kind { + Identifier => { + if let Some(op) = self.tokens().pop_front() { + fields.push(op); + } + } + Keyword if t.value == "FROM" || t.value == "VALUES" => { + if fields.is_empty() { + errored!(Syntax, "read FROM without any * or fields in query.") + } + break; + } + ParenthesisClose => { + self.tokens().pop_front(); + break; + } + Operator if t.value == "*" => { + self.tokens().pop_front(); + break; + } + ParenthesisOpen => { + self.tokens().pop_front(); + } + _ => unexpected_token_in_stage("COLUMN", t)?, + } + } + 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); + } + 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(), + kind, + }; + if let Some(t) = self.tokens().front() { + if t.kind != expected.kind || t.value != expected.value.to_uppercase() { + errored!(Syntax, "expected {:?} token, got: {:?}", expected, t) + } + } else { + errored!(Syntax, "got None when expecting: {:?}", expected) + } + 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() { + "SELECT" => Select, + "INSERT INTO" => Insert, + "UPDATE" => Update, + "DELETE" => Delete, + _ => Unknown, + }, + None => Unknown, + } +} + +/// 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, + operation: Operation, +) -> Result<(), Errored> { + 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(()) +} + +/// 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, + "unexpected token while parsing {} fields: {:?}", + stage, + token + ) +} diff --git a/src/query/builder/select.rs b/src/query/builder/select.rs new file mode 100644 index 0000000..d516e94 --- /dev/null +++ b/src/query/builder/select.rs @@ -0,0 +1,230 @@ +use crate::query::builder::{unexpected_token_in_stage, validate_keywords, Builder}; +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] = &[ + "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() { + if t.kind != Identifier { + unexpected_token_in_stage("ORDER_BY", &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" => { + new_order.kind = if next.value == "DESC" { Desc } else { Asc }; + self.tokens.pop_front(); + } + _ => {} + } + } + ordering.push(new_order) + } + Ok(ordering) + } +} + +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()?; + query.operation = Select; + query.columns = self.parse_columns()?; + query.table = self.parse_table(Select)?; + 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(_) => self.expect_none()?, + } + 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) + } +} + +#[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 new file mode 100644 index 0000000..37fa48c --- /dev/null +++ b/src/query/builder/update.rs @@ -0,0 +1,195 @@ +use crate::errored; +use crate::query::builder::expression::ExpressionBuilder; +use crate::query::builder::{validate_keywords, Builder}; +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"]; + +/// 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![]; + 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 { + /// 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()?; + query.operation = Update; + query.table = self.parse_table(Update)?; + query.updates = self.parse_updates()?; + match self.peek_expecting("WHERE", Keyword) { + Ok(_) => { + query.conditions = self.parse_where()?; + } + Err(_) => self.expect_none()?, + } + 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) + } +} + +#[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 new file mode 100644 index 0000000..8558918 --- /dev/null +++ b/src/query/executor/delete.rs @@ -0,0 +1,47 @@ +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 { + /// 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)?; + 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.read_new_row(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 new file mode 100644 index 0000000..18a34c3 --- /dev/null +++ b/src/query/executor/insert.rs @@ -0,0 +1,38 @@ +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}; + +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); + 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); + row.clear()?; + row.insert_values(&self.query.columns, fields)?; + writeln!(table, "{}", row.as_csv_row())? + } + Ok(()) + } +} diff --git a/src/query/executor/mod.rs b/src/query/executor/mod.rs new file mode 100644 index 0000000..99241d8 --- /dev/null +++ b/src/query/executor/mod.rs @@ -0,0 +1,85 @@ +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_path; +use std::path::{Path, PathBuf}; + +mod delete; +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); + match executor.query.operation { + 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 new file mode 100644 index 0000000..83a8cb0 --- /dev/null +++ b/src/query/executor/select.rs @@ -0,0 +1,152 @@ +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}; + +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); + let header = extract_header(&mut reader)?; + self.validate_projection(&header)?; + let mut matched_rows: Vec = vec![]; + for line in reader.lines() { + let l = line?; + let fields = split_csv(&l); + let mut row = Row::new(&header); + row.read_new_row(fields)?; + if row.matches_condition(&self.query)? { + matched_rows.push(row) + } + } + self.sort_rows(&mut matched_rows, &header)?; + self.output_projection(&header, &matched_rows); + 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. + /// + /// 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. + /// + /// # 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) { + errored!( + Column, + "order by failed, column {} does not exist", + &order.field.value + ) + } + } + 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) { + 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 + }); + 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`). + /// + /// 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_projection(&self, header: &[String], matched_rows: &[Row]) { + let mut columns = vec![]; + if self.query.columns.is_empty() { + println!("{}", header.join(",")); + } else { + columns = self + .query + .columns + .iter() + .map(|t| t.value.to_string()) + .collect(); + println!("{}", columns.join(",")); + } + for row in matched_rows { + row.print_projection(&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; + 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 new file mode 100644 index 0000000..92890f7 --- /dev/null +++ b/src/query/executor/update.rs @@ -0,0 +1,48 @@ +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 { + /// 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)?; + 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.read_new_row(fields)?; + if row.matches_condition(&self.query)? { + row.apply_updates(&self.query.updates)?; + writeln!(writer, "{}", row.as_csv_row())? + } else { + writeln!(writer, "{}", l)? + } + } + delete_temp_file(&self.table_path, &temp_path)?; + Ok(()) + } +} diff --git a/src/query/mod.rs b/src/query/mod.rs new file mode 100644 index 0000000..7fe44db --- /dev/null +++ b/src/query/mod.rs @@ -0,0 +1,15 @@ +pub mod builder; +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!(Default, "query is empty."); + } + Ok(()) +} diff --git a/src/query/structs/comparator.rs b/src/query/structs/comparator.rs new file mode 100644 index 0000000..e355bc1 --- /dev/null +++ b/src/query/structs/comparator.rs @@ -0,0 +1,246 @@ +use crate::errored; +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; + +/// 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, + 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), + } + } + + /// 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, + 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), + } + } + + /// 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, + 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), + } + } + + /// 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, + ) -> 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 {:?}", + this, + other + ), + } + } +} + +#[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 new file mode 100644 index 0000000..69a030b --- /dev/null +++ b/src/query/structs/expression.rs @@ -0,0 +1,332 @@ +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::{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] + Empty, + Leaf(Token), + Statement { + operator: ExpressionOperator, + left: Box, + right: Box, + }, +} + +/// 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] + None, + Equals, + NotEquals, + GreaterThan, + LessThan, + GreaterOrEqual, + LessOrEqual, + And, + Or, + 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), + Str(String), + Bool(bool), +} + +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)), + 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) + } + } + } + + /// 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, + 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, "expression members must match in type."), + } + } + + /// 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, + ) -> 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 => errored!(Column, "column {} does not exist", t.value), + } + } + + /// 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) { + (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 { + 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) + } + } + } +} + +#[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/mod.rs b/src/query/structs/mod.rs new file mode 100644 index 0000000..8a5056d --- /dev/null +++ b/src/query/structs/mod.rs @@ -0,0 +1,7 @@ +pub mod comparator; +pub mod expression; +pub mod operation; +pub mod ordering; +pub mod query; +pub mod row; +pub mod token; diff --git a/src/query/structs/operation.rs b/src/query/structs/operation.rs new file mode 100644 index 0000000..890a855 --- /dev/null +++ b/src/query/structs/operation.rs @@ -0,0 +1,17 @@ +/// 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, + Select, + Update, + Delete, + Insert, +} diff --git a/src/query/structs/ordering.rs b/src/query/structs/ordering.rs new file mode 100644 index 0000000..e034401 --- /dev/null +++ b/src/query/structs/ordering.rs @@ -0,0 +1,47 @@ +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, + Desc, +} + +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(), + 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..a4b4e33 --- /dev/null +++ b/src/query/structs/query.rs @@ -0,0 +1,131 @@ +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::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}; + +/// 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, "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(), + Insert => InsertBuilder::new(tokens).build(), + } + } +} + +impl Default for Query { + /// Devuelve un valor default para `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(); + 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, "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, "Actualizaciones: {:?}", self.updates)?; + writeln!(f, "Condiciones: {:?}", self.conditions)?; + writeln!(f, "Ordenamiento: {:?}", self.ordering) + } +} + +impl Debug for Query { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + 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(), + "debería dar error con un token desconocido." + ); + match result { + 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 new file mode 100644 index 0000000..5b1a3d5 --- /dev/null +++ b/src/query/structs/row.rs @@ -0,0 +1,461 @@ +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; + +/// 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, + values: HashMap::new(), + } + } + + /// 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 { + errored!( + Column, + "column {} does not exist in table with fields: {:?}", + key, + self.header + ) + } + 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())? + } + 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() { + let k = &field.value; + let v = &value.value; + self.set(k, v.to_string())? + } else { + errored!(Default, "error while updating values.") + } + } + 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!( + Table, + "new row has ({}) fields but table needs ({}).", + values.len(), + self.header.len() + ); + } + for (key, value) in self.header.iter().zip(values) { + self.set(key, value)?; + } + 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)? + } + Ok(()) + } + + /// 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(""); + projection.push(value); + } + 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!["id".to_string()]); + /// ``` + pub fn print_projection(&self, columns: &Vec) { + if columns.is_empty() { + println!("{}", self.as_csv_row()); + } else { + println!("{}", self.as_csv_projection(columns)); + } + } + + /// 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), + _ => errored!(Syntax, "query condition evaluates to non-boolean value."), + } + } +} + +#[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::*; + use std::default::Default; + + #[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_row(); + 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/structs/token.rs b/src/query/structs/token.rs new file mode 100644 index 0000000..85f38b6 --- /dev/null +++ b/src/query/structs/token.rs @@ -0,0 +1,69 @@ +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, + String, + Number, + Operator, + Identifier, + ParenthesisOpen, + ParenthesisClose, + Keyword, +} + +impl Default for Token { + /// Crea un token con valores predeterminados. + fn default() -> Self { + Self { + value: String::new(), + kind: Unknown, + } + } +} diff --git a/src/query/tokenizer.rs b/src/query/tokenizer.rs new file mode 100644 index 0000000..c2a0891 --- /dev/null +++ b/src/query/tokenizer.rs @@ -0,0 +1,511 @@ +use crate::errored; +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] = &["*", "=", "<", ">", "!", ">=", "<=", "!="]; + +const IGNORABLE_CHARS: &[char] = &[' ', ',', ';', '\0', '\n']; + +const RESERVED_KEYWORDS: &[&str] = &[ + "SELECT", + "UPDATE", + "DELETE", + "INSERT INTO", + "SET", + "VALUES", + "ORDER BY", + "DESC", + "ASC", + "FROM", + "WHERE", + "AND", + "OR", + "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, + state: TokenizerState, + 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] + Begin, + IdentifierOrKeyword, + Operator, + NumberLiteral, + StringLiteral, + OpenParenthesis, + CloseParenthesis, + Complete, +} + +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, + state: Begin, + parenthesis_count: 0, + } + } + + /// 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(); + 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)?, + OpenParenthesis | CloseParenthesis => token = self.tokenize_parenthesis(sql)?, + StringLiteral => { + self.i += c.len_utf8(); + token = self.tokenize_quoted(sql)?; + } + Complete => { + out.push(token); + token = Token::default(); + self.reset() + } + } + } + if token.kind != Unknown { + out.push(token); + } + if self.parenthesis_count != 0 { + errored!(Syntax, "unclosed parentheses found.") + } + 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(), + 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, + "could not tokenize char: {} at index: {}.", + c, + self.i + ), + } + 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(); + 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) + } + + /// 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(); + self.state = Complete; + return Ok(Token { + value: word, + kind: Keyword, + }); + } + 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(); + self.state = Complete; + Ok(Token { + value: op, + kind: TokenKind::Operator, + }) + } else { + errored!( + Syntax, + "unrecognized operator {} at index: {}", + char_at(self.i, sql), + self.i + ); + } + } + + /// 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() { + if char == '\'' { + let end = start + index; + self.i = end + 1; + let quoted = &sql[start..end]; + self.state = Complete; + return Ok(Token { + value: String::from(quoted), + kind: TokenKind::String, + }); + } + } + 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, + tokens: &[&str], + matches_kind: F, + ) -> Option + where + 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 && !matches_kind(next_char) { + return Some(token.to_uppercase()); + } + } + } + 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, + 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); + } + let token = &sql[start..self.i]; + self.state = Complete; + Ok(Token { + value: String::from(token), + kind: output_kind, + }) + } + + /// 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()) +} + +#[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/errors.rs b/src/utils/errors.rs new file mode 100644 index 0000000..f02cd58 --- /dev/null +++ b/src/utils/errors.rs @@ -0,0 +1,94 @@ +use crate::utils::errors::Errored::*; +use std::error::Error; +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) => { + return Err($err_type(format!($msg))) + }; + ($err_type:ident, $fmt:expr, $($arg:tt)*) => { + return Err($err_type(format!($fmt, $($arg)*))) + }; +} + +/// 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), + Table(String), + Default(String), +} + +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 { + 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 { + 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 new file mode 100644 index 0000000..3380612 --- /dev/null +++ b/src/utils/files.rs @@ -0,0 +1,293 @@ +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::io::{Read, Seek, SeekFrom, Write}; +use std::path::{Path, PathBuf}; +use std::{fs, thread}; + +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() { + errored!( + Default, + "table {} does not exist in directory: {}", + table_name, + dir_path.display() + ); + } + Ok(table_path) +} + +/// Genera un identificador único para un archivo temporal. +/// +/// # Retorna +/// +/// Devuelve un `u64` que representa el identificador único. +/// +/// # Ejemplo +/// +/// ```rust +/// use rustic_sql::utils::files::get_temp_id; +/// let id = get_temp_id(); +/// println!("{}", id); +/// ``` +pub fn get_temp_id() -> u64 { + let id = thread::current().id(); + let mut hasher = DefaultHasher::new(); + id.hash(&mut hasher); + 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 + .with_file_name(format!("{}_{}", table_name, id)) + .with_extension(TEMP_EXTENSION); + Ok(( + File::options() + .create_new(true) + .read(true) + .write(true) + .truncate(true) + .open(&table_path)?, + table_path, + )) +} + +/// 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 { + errored!(Default, "tried to delete non_temporary file.") + } + } + fs::rename(temp_path, table_path)?; + 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 { + 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(()) +} + +/// 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() { + 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) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extract_header() { + 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"]); + } + + #[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_unit_tables"); + 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/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); + assert!(result.is_ok()); + } + + #[test] + fn test_delete_non_temporary_file_error() { + 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/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/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_unit_tables/"); + assert!(result.is_err()); + } + + #[test] + fn test_validate_path_not_a_directory() { + let result = validate_path("/test/tables/clientes.csv"); + assert!(result.is_err()); + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..cb26b62 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod errors; +pub mod files; 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/insert.rs b/tests/insert.rs new file mode 100644 index 0000000..3b76e66 --- /dev/null +++ b/tests/insert.rs @@ -0,0 +1,64 @@ +use crate::utils::RusticSQLTest; + +pub mod utils; + +#[test] +fn test_insert_user_all_fields() { + 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"; + test.assert_row( + select_query, + &["14", "Solidus Snake", "solidus.snake@mgs.com", "40"], + ) +} + +#[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(); + 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"; + test.assert_row(select_query, &["15", "Raiden", "raiden@mgs.com", "33"]); + let select_query = "SELECT * FROM users WHERE user_id = 16"; + 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::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'"; + test.assert_row(select_query, &["", "", "liquid.snake@mgs.com", "35"]); +} + +#[test] +fn test_insert_invalid_column() { + 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"))); +} + +#[test] +fn test_insert_invalid_table() { + 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()); + assert!(result.is_err_and(|e| e.to_string().contains("table"))); +} 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 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/integration_tables/users.csv b/tests/integration_tables/users.csv new file mode 100644 index 0000000..d4cb26d --- /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 \ No newline at end of file diff --git a/tests/select.rs b/tests/select.rs new file mode 100644 index 0000000..1277e41 --- /dev/null +++ b/tests/select.rs @@ -0,0 +1,112 @@ +use crate::utils::RusticSQLTest; + +pub mod utils; + +#[test] +fn test_empty_query() { + 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::default(); + 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::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"], + 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 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..]); +} + +#[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(); + 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 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..]); +} + +#[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(); + 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::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"))) +} + +#[test] +fn test_select_with_invalid_table() { + 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/unit_tables/clientes.csv b/tests/unit_tables/clientes.csv new file mode 100644 index 0000000..85310a6 --- /dev/null +++ b/tests/unit_tables/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/unit_tables/ordenes.csv b/tests/unit_tables/ordenes.csv new file mode 100644 index 0000000..bf38946 --- /dev/null +++ b/tests/unit_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 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 new file mode 100644 index 0000000..ce85120 --- /dev/null +++ b/tests/utils/mod.rs @@ -0,0 +1,109 @@ +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; + +pub struct RusticSQLTest { + temp_dir: PathBuf, +} + +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()); + 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 { + 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 + } + + 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) { + 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(), + self.temp_dir.to_str().unwrap().to_string(), + query, + ] + } + + pub fn run_for(&self, query: String) -> Result<(), Box> { + run(self.args_for(query)) + } + + pub fn run_and_get_rows(&self, query: String) -> Vec { + let args = self.args_for(query); + let output = Command::new(&args[0]) + .arg(&args[1]) + .arg(&args[2]) + .output() + .unwrap(); + let raw = String::from_utf8(output.stdout).unwrap(); + raw.trim() + .split("\n") + .map(|s| s.to_string()) + .collect::>() + } +} + +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 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(), + } + } +} + +impl Drop for RusticSQLTest { + fn drop(&mut self) { + self.tear_down() + } +}