diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..c7cfaa8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,6 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +/inspectionProfiles/Project_Default.xml diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..50979f2 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/tp-individual-taller-9508.iml b/.idea/tp-individual-taller-9508.iml new file mode 100644 index 0000000..bbe0a70 --- /dev/null +++ b/.idea/tp-individual-taller-9508.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..48d8d5c --- /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 = "tp_individual_taller_9508" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..662d1d1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "tp_individual_taller_9508" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/README.md b/README.md new file mode 100644 index 0000000..743e30f --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# tp-individual-taller-95.08 +repo xa el tp individual + +Crear un nuevo proyecto: cargo new + +I Crear un nuevo proyecto en un directorio existente: cargo init +(basta con ejecutarlo estando parado en ese directorio) + +I Compilar el proyecto: cargo build + +I Compilar el proyecto en modo release: cargo build –-release + +I Ejecutar el proyecto: cargo run + +I Ejecutar los tests: cargo test + +I Generar la documentación HTML: cargo doc + +I Analizar el proyecto, sin compilar: cargo check + +I Formatear el código: cargo fmt + +I linter: cargo clippy diff --git a/output.csv b/output.csv new file mode 100644 index 0000000..32cdf30 --- /dev/null +++ b/output.csv @@ -0,0 +1 @@ +InvalidTable: Ocurrio un error al procesar la tabla. diff --git a/src/comandos.rs b/src/comandos.rs new file mode 100644 index 0000000..c4a0e89 --- /dev/null +++ b/src/comandos.rs @@ -0,0 +1,51 @@ +//! Módulo para gestionar la ejecución de comandos SQL. +//! +//! Este módulo define un `enum` llamado `Comando` que puede representar operaciones SQL de tipo `SELECT`, `INSERT`, `UPDATE` y `DELETE`. +//! También proporciona métodos para ejecutar estos comandos y para imprimir los resultados de una consulta `SELECT`. +use crate::delete::Delete; +use crate::errores::SqlError; +use crate::insert::Insert; +use crate::select::Select; +use crate::update::Update; + +/// Enum que representa los diferentes comandos SQL soportados. +pub enum Comando { + Select(Select), + Insert(Insert), + Update(Update), + Delete(Delete), +} + +impl Comando { + pub fn ejecutar(&self, ruta_carpeta: &str) -> Result>, SqlError> { + match self { + Comando::Select(select) => select.ejecutar(ruta_carpeta), + Comando::Update(update) => update.ejecutar(ruta_carpeta), + Comando::Insert(insert) => insert.ejecutar(ruta_carpeta), + Comando::Delete(delete) => delete.ejecutar(ruta_carpeta), + } + } + /// Imprime los resultados de una consulta `SELECT`. + /// + /// Esta función imprime los resultados de una consulta si el comando es un `Select`. Para otros tipos de comando (`Insert`, `Update`, `Delete`), + /// la función no hace mnada + /// + /// # Parámetros + /// + /// - `results`: Una referencia a un vector de vectores de `String` que contiene los resultados de la consulta. + pub fn imprimir_resultados(&self, results: &[Vec]) { + match self { + Comando::Select(_select) => Select::imprimir_resultados(results), + Comando::Update(_update) => { + //nadaaa + } + // } + Comando::Insert(_insert) => { + // nadaaa + } + Comando::Delete(_delete) => { + // nadaaa + } + } + } +} diff --git a/src/delete.rs b/src/delete.rs new file mode 100644 index 0000000..b82e828 --- /dev/null +++ b/src/delete.rs @@ -0,0 +1,451 @@ +//! Módulo para gestionar la consulta delete +//! +//! Este módulo define el `struct` `Delete` que representa un comando SQL de eliminación de registros en una tabla CSV. +//! Proporciona métodos para ejecutar la eliminación basada en restricciones y reemplazar el archivo original con una versión modificada. +//! Para ello crea un archivo temporal donde guarda los registros que no deben ser eliminados +use crate::errores::SqlError; +use std::fs::{self, File}; +use std::io::{BufRead, BufReader, Lines, Write}; +use std::iter::Peekable; +use std::str::Chars; + +/// tabla: Tabla es la tabla con la que se esta trabjando +/// resstricciones: las restricciones del WHERE +pub struct Delete { + pub tabla: String, + pub restricciones: Option, +} +impl Delete { + /// Ejecuta el comando de eliminación en la tabla especificada. + /// + /// Lee el archivo CSV correspondiente a la tabla, aplica las restricciones para determinar qué registros eliminar, + /// y escribe los registros restantes en un archivo temporal. Luego, reemplaza el archivo original con el archivo temporal. + /// + /// # Retorna + /// + /// Un `Result` que contiene un `Vec>` con las filas que cumplieron con las restricciones y fueron eliminadas, + /// o un `SqlError` en caso de error. + pub fn ejecutar(&self, ruta_carpeta_tablas: &str) -> Result>, SqlError> { + let ruta_archivo = format!("{}/{}.csv", ruta_carpeta_tablas, self.tabla); + let ruta_temporal = format!("{}/{}.tmp", ruta_carpeta_tablas, self.tabla); + + let archivo = match Self::abrir_archivo_tabla(&ruta_archivo) { + Ok(value) => value, + Err(value) => return value, + }; + + let reader = BufReader::new(archivo); + + let mut archivo_temporal = match Self::crear_archivo_temporal(&ruta_temporal) { + Ok(value) => value, + Err(value) => return value, + }; + + // encabezadosss + let mut lineas = reader.lines(); + let encabezados = match Self::hallar_encabezados(&mut lineas) { + Ok(value) => value, + Err(value) => return value, + }; + let vector_encabezados = Self::armar_vector_encabezados(encabezados); + + if let Some(value) = Self::escribir_encabezados(&mut archivo_temporal, &vector_encabezados) + { + return value; + } + + let mut filas_resultantes = Vec::new(); + + self.borrar_linea_a_linea( + &ruta_temporal, + &mut archivo_temporal, + &mut lineas, + &vector_encabezados, + &mut filas_resultantes, + )?; + + if let Some(value) = Self::reemplazar_original_por_temp(&ruta_archivo, &ruta_temporal) { + return value; + } + + Ok(filas_resultantes) + } + + fn borrar_linea_a_linea( + &self, + ruta_temporal: &String, + archivo_temporal: &mut File, + lineas: &mut Lines>, + vector_encabezados: &[String], + filas_resultantes: &mut Vec>, + ) -> Result<(), SqlError> { + // Leo el resto del archivo y veo con cuáles me quedo + for linea in lineas { + let linea = match linea { + Ok(l) => l, + Err(_err) => return Err(SqlError::InvalidTable), + }; + + let fila: Vec = linea.split(',').map(|s| s.trim().to_string()).collect(); + + if let Err(err) = self.aplicar_restricciones(&fila, vector_encabezados) { + //borro el archivo temporal si hay error en las restricciones y devuelvo error + let _ = std::fs::remove_file(ruta_temporal); + return Err(err); + } else if !self.aplicar_restricciones(&fila, vector_encabezados)? { + // Si la fila no cumple con las restricciones, la escribimos en el archivo temporal + if let Err(_err) = writeln!(archivo_temporal, "{}", linea) { + return Err(SqlError::Error( + "Error al escribir el archivo temporal".to_string(), + )); + } + } else { + // Fila que cumple con las restricciones y se elimina + filas_resultantes.push(fila); + } + } + Ok(()) + } + + fn crear_archivo_temporal( + ruta_temporal: &String, + ) -> Result>, SqlError>> { + // Creo un archivo temporal + let archivo_temporal = match File::create(ruta_temporal) { + Ok(file) => file, + Err(_err) => { + return Err(Err(SqlError::Error( + "Error al crear el archivo temporal:".into(), + ))); + } + }; + Ok(archivo_temporal) + } + + fn hallar_encabezados( + lineas: &mut Lines>, + ) -> Result>, SqlError>> { + let encabezados = match lineas.next() { + Some(Ok(line)) => line, + Some(Err(_err)) => { + return Err(Err(SqlError::InvalidTable)); + } + None => { + // archivo vacío + return Err(Err(SqlError::InvalidTable)); + } + }; + Ok(encabezados) + } + + fn reemplazar_original_por_temp( + ruta_archivo: &String, + ruta_temporal: &String, + ) -> Option>, SqlError>> { + // Reemplazo el archivo original con el archivo temporal + if let Err(_err) = fs::rename(ruta_temporal, ruta_archivo) { + return Some(Err(SqlError::Error( + "Error al reemplazar el archivo original con el archivo temporal".to_string(), + ))); + } + None + } + + fn armar_vector_encabezados(encabezados: String) -> Vec { + let encabezados: Vec = encabezados + .split(',') + .map(|s| s.trim().to_string()) + .collect(); + encabezados + } + + fn escribir_encabezados( + archivo_temporal: &mut File, + encabezados: &[String], + ) -> Option>, SqlError>> { + // Escribo encabezados al archivo de salida + if let Err(_err) = writeln!(archivo_temporal, "{}", encabezados.join(",")) { + return Some(Err(SqlError::Error( + "Error al escribir encabezados en el archivo temporal:".into(), + ))); + } + None + } + + fn abrir_archivo_tabla( + ruta_archivo: &String, + ) -> Result>, SqlError>> { + // Abro archivo tabla + let archivo = match File::open(ruta_archivo) { + Ok(file) => file, + Err(_err) => { + return Err(Err(SqlError::InvalidTable)); + } + }; + Ok(archivo) + } + + fn aplicar_restricciones( + &self, + registro: &[String], + encabezados: &[String], + ) -> Result { + if let Some(restricciones) = &self.restricciones { + let tokens = self.tokenizar(restricciones); + let postfix = self.infix_a_postfix(&tokens)?; + let resultado = self.evaluar_postfix(&postfix, registro, encabezados)?; + Ok(resultado) + } else { + Ok(true) + } + } + + fn sacar_espacios_alrededor_operadores(&self, restricciones: &str) -> String { + let mut restricciones_limpio = String::new(); + let mut chars = restricciones.chars().peekable(); + + while let Some(ch) = chars.next() { + match ch { + '=' | '<' | '>' => { + //Eliminamos espacios antes del operador + if let Some(last_char) = restricciones_limpio.chars().last() { + if last_char == ' ' { + restricciones_limpio.pop(); + } + } + //Agregamos el operador al resultado + restricciones_limpio.push(ch); + + //Ignoramos cualquier espacio después del operador + while let Some(&next_ch) = chars.peek() { + if next_ch == ' ' { + chars.next(); + } else { + break; + } + } + } + _ => { + restricciones_limpio.push(ch); + } + } + } + + restricciones_limpio + } + + //Tokeniza las restricciones + fn tokenizar(&self, restricciones: &str) -> Vec { + let restricciones_limpio = self.sacar_espacios_alrededor_operadores(restricciones); + let mut chars = restricciones_limpio.chars().peekable(); + + let mut tokens = Vec::new(); + let mut token_actual = String::new(); + + let keywords = ["AND", "OR", "NOT", "(", ")"]; + + Self::loop_tokenizar(&mut tokens, &mut token_actual, keywords, &mut chars); + + if !token_actual.is_empty() { + tokens.push(token_actual.trim().to_string()); + } + + tokens.retain(|token| !token.is_empty()); + + tokens + } + + fn loop_tokenizar( + tokens: &mut Vec, + token_actual: &mut String, + keywords: [&str; 5], + chars: &mut Peekable, + ) { + for ch in chars.by_ref() { + match ch { + ' ' if !token_actual.is_empty() => { + tokens.push(token_actual.trim().to_string()); + token_actual.clear(); + } + '(' | ')' => { + if !token_actual.is_empty() { + tokens.push(token_actual.trim().to_string()); + token_actual.clear(); + } + tokens.push(ch.to_string()); + } + _ => { + token_actual.push(ch); + let token_upper = token_actual.trim().to_uppercase(); + if keywords.contains(&token_upper.as_str()) { + tokens.push(token_upper); + token_actual.clear(); + } + } + } + } + } + + //Convierte la notación infix (con paréntesis) a notación postfija (sin parentesis) + //infix: (A OR B) AND (C OR D) ====> postfix: A B OR C D OR AND + fn infix_a_postfix(&self, tokens: &[String]) -> Result, SqlError> { + let mut resultado = Vec::new(); + let mut operadores = Vec::new(); + + let precedencia = |op: &str| match op { + "NOT" => 3, + "AND" => 2, + "OR" => 1, + "(" => 0, + ")" => 0, + _ => -1, + }; + + for token in tokens.iter() { + match token.as_str() { + "(" => operadores.push(token.to_string()), + ")" => { + while let Some(op) = operadores.pop() { + if op == "(" { + break; + } + resultado.push(op); + } + } + "AND" | "OR" | "NOT" => { + while let Some(op) = operadores.last() { + if precedencia(op) >= precedencia(token) { + // el operador en la cima de la pila se extrae de la pila + // y se coloca en el output antes de agregar el nuevo operador. + if let Some(op) = operadores.pop() { + resultado.push(op); //saco + } else { + return Err(SqlError::InvalidSintax); + } + } else { + break; + } + } + operadores.push(token.to_string()); + } + _ => resultado.push(token.to_string()), + } + } + + while let Some(op) = operadores.pop() { + resultado.push(op); + } + + Ok(resultado) + } + + //Evalúo la expresión en notación postfija + fn evaluar_postfix( + &self, + tokens: &[String], + registro: &[String], + encabezados: &[String], + ) -> Result { + let mut stack = Vec::new(); + + for token in tokens.iter() { + match token.as_str() { + "AND" | "OR" | "NOT" => { + let derecha = stack.pop().ok_or(SqlError::InvalidSintax)?; + let izquierda = if token != "NOT" { + stack.pop().ok_or(SqlError::InvalidSintax)? + } else { + String::new() + }; + let resultado = match token.as_str() { + "AND" => { + self.aplicar_condicion(&izquierda, registro, encabezados)? + && self.aplicar_condicion(&derecha, registro, encabezados)? + } + "OR" => { + self.aplicar_condicion(&izquierda, registro, encabezados)? + || self.aplicar_condicion(&derecha, registro, encabezados)? + } + "NOT" => !self.aplicar_condicion(&derecha, registro, encabezados)?, + _ => false, + }; + stack.push(resultado.to_string()); + } + _ => + //Proceso la condición directa + { + if stack.is_empty() && tokens.len() == 1 { + // Si el primer token es la única condición, evalúo directamente + let result = self.aplicar_condicion(token, registro, encabezados)?; + return Ok(result); + } else { + // Para otros tokens, los mete al stack para ser procesados con operadores + stack.push(token.to_string()); + } + } + } + } + //si esta vacio el stack error, sino devuelvo el resultado (ult valor del stack) + Ok(stack.pop().ok_or(SqlError::InvalidSintax)? == "true") + } + + //Aplico condición simple (comparaciones > < = <= >=) + fn aplicar_condicion( + &self, + condicion: &str, + registro: &[String], + encabezados: &[String], + ) -> Result { + if condicion == "true" { + return Ok(true); + } else if condicion == "false" { + return Ok(false); + } + + // Parseo la condición para encontrar operadores de comparación + let (columna, operador, valor) = if let Some(pos) = condicion.find(">=") { + (&condicion[..pos].trim(), ">=", &condicion[pos + 2..].trim()) + } else if let Some(pos) = condicion.find("<=") { + (&condicion[..pos].trim(), "<=", &condicion[pos + 2..].trim()) + } else if let Some(pos) = condicion.find('>') { + (&condicion[..pos].trim(), ">", &condicion[pos + 1..].trim()) + } else if let Some(pos) = condicion.find('<') { + (&condicion[..pos].trim(), "<", &condicion[pos + 1..].trim()) + } else if let Some(pos) = condicion.find('=') { + (&condicion[..pos].trim(), "=", &condicion[pos + 1..].trim()) + } else { + return Err(SqlError::Error( + "Operador no válido en la condición".to_string(), + )); + }; + + let valor = valor.trim().trim_matches('\''); + + Self::ejecutar_comparacion(®istro, encabezados, columna, operador, valor) + } + + fn ejecutar_comparacion( + registro: &&[String], + encabezados: &[String], + columna: &&str, + operador: &str, + valor: &str, + ) -> Result { + // Encuentro el índice de la columna, o devuelvo un error si no se encuentra + if let Some(indice) = encabezados.iter().position(|enc| enc == columna) { + let valor_registro = ®istro[indice]; + let resultado = match operador { + "=" => valor_registro.as_str() == valor, + ">" => valor_registro.as_str() > valor, + "<" => valor_registro.as_str() < valor, + "<=" => valor_registro.as_str() <= valor, + ">=" => valor_registro.as_str() >= valor, + _ => false, + }; + + Ok(resultado) + } else { + Err(SqlError::InvalidColumn) + } + } +} diff --git a/src/errores.rs b/src/errores.rs new file mode 100644 index 0000000..46fbffc --- /dev/null +++ b/src/errores.rs @@ -0,0 +1,32 @@ +//! Módulo para definir errores específicos +//! +//! Este módulo define el `enum` `SqlError` que representa los diferentes tipos de errores que pueden ocurrir durante +//! el procesamiento de comandos SQL. +//! También implementa el trait `Display` para poder mostrar correctamente estos errores. +use std::fmt; + +#[derive(Debug)] +pub enum SqlError { + InvalidTable, + InvalidColumn, + InvalidSintax, + Error(String), +} +impl fmt::Display for SqlError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SqlError::InvalidTable => { + write!(f, "INVALID_TABLE: Ocurrió un error al procesar la tabla.") + } + SqlError::InvalidColumn => write!( + f, + "INVALID_COLUMN: Ocurrió un error al procesar alguna columna." + ), + SqlError::InvalidSintax => write!( + f, + "INVALID_SYNTAX: Hay un error en la sintaxis de la consulta" + ), + SqlError::Error(msg) => write!(f, "ERROR: {}", msg), + } + } +} diff --git a/src/insert.rs b/src/insert.rs new file mode 100644 index 0000000..fb1851c --- /dev/null +++ b/src/insert.rs @@ -0,0 +1,149 @@ +//! Módulo para gestionar la consulta insert +//! +//! Este módulo define el `struct` `Insert` y roporciona métodos para ejecutar la inserción de +//! nuevos registros en la tabla, verificando la validez de las columnas +//! y los encabezados del archivo CSV, y agregando las nuevas filas al final del archivo. +use crate::errores::SqlError; +use std::fs::{File, OpenOptions}; +use std::io::{BufRead, BufReader, BufWriter, Write}; +use std::path::{Path, PathBuf}; + +///Columnas: representa las columnas de los valores que se desean insertar +///Valores: Listado de registros a insertar +///Tabla: tabla +pub struct Insert { + pub tabla: String, + pub columnas: Vec, + pub valores: Vec>, +} + +impl Insert { + /// Ejecuta el comando insert + /// + /// Abre el archivo CSV correspondiente a la tabla, verifica que las columnas de la inserción coincidan con las del archivo, + /// y agrega las nuevas filas al final del archivo. Retorna las filas que se han insertado o un error en caso de fallo. + /// # Retorna + /// Un `Result` que contiene un `Vec>` con las filas que han sido insertadas, + /// o un `SqlError` en caso de error. + pub fn ejecutar(&self, ruta_carpeta_tablas: &str) -> Result>, SqlError> { + let archivo_csv = Path::new(ruta_carpeta_tablas).join(format!("{}.csv", self.tabla)); + + let file = match Self::abrir_archivo(&archivo_csv) { + Ok(value) => value, + Err(value) => return value, + }; + + let mut reader = BufReader::new(file); + let mut encabezados = String::new(); + + if let Err(_err) = reader.read_line(&mut encabezados) { + return Err(SqlError::InvalidTable); + } + + let vector_encabezados = Self::armar_vector_encabezados(&mut encabezados); + + let columnas_indices = match self.chequear_columnas_estan_ok(&vector_encabezados) { + Ok(value) => value, + Err(value) => return value, + }; + + // Abro el archivo en modo append para agregar los nuevos + let file = match OpenOptions::new().append(true).open(&archivo_csv) { + Ok(file) => file, + Err(_err) => { + return Err(SqlError::InvalidTable); + } + }; + + let mut writer = BufWriter::new(file); + let mut filas_insertadas = Vec::with_capacity(self.valores.len()); + + if let Some(value) = self.escribir_archivo( + vector_encabezados, + columnas_indices, + &mut writer, + &mut filas_insertadas, + ) { + return value; + } + // devuelvo pa q no tire error + Ok(filas_insertadas) + } + + fn abrir_archivo(archivo_csv: &PathBuf) -> Result>, SqlError>> { + // Leo archivo para obtener los encabezados + let file = match File::open(archivo_csv) { + Ok(file) => file, + Err(_err) => { + return Err(Err(SqlError::InvalidTable)); + } + }; + Ok(file) + } + + fn escribir_archivo( + &self, + vector_encabezados: Vec, + columnas_indices: Vec, + writer: &mut BufWriter, + filas_insertadas: &mut Vec>, + ) -> Option>, SqlError>> { + // Escribo archivo + for fila in &self.valores { + let mut fila_nueva = vec!["".to_string(); vector_encabezados.len()]; + + for (i, col_index) in columnas_indices.iter().enumerate() { + if i < fila.len() { + fila_nueva[*col_index] = fila[i].to_string(); + } + } + if let Some(value) = Self::escribir_linea_en_archivo(writer, &mut fila_nueva) { + return Some(value); + } + filas_insertadas.push(fila_nueva); + } + None + } + + fn escribir_linea_en_archivo( + writer: &mut BufWriter, + fila_nueva: &mut [String], + ) -> Option>, SqlError>> { + //la agrego + if let Err(_err) = writeln!(writer, "{}", fila_nueva.join(",")) { + return Some(Err(SqlError::Error( + "Error al escribir lineas en el archivo:".into(), + ))); + } + None + } + + fn chequear_columnas_estan_ok( + &self, + vector_encabezados: &[String], + ) -> Result, Result>, SqlError>> { + // Verifio que las columnas de la consulta existan en el archivo + let mut columnas_indices = Vec::with_capacity(self.columnas.len()); + for (index, header) in vector_encabezados.iter().enumerate() { + if self.columnas.contains(header) { + columnas_indices.push(index); + } else { + return Err(Err(SqlError::InvalidColumn)); + } + } + //y que no haya columnas de mas ni de menos + if columnas_indices.len() != self.columnas.len() { + return Err(Err(SqlError::InvalidColumn)); + } + Ok(columnas_indices) + } + + fn armar_vector_encabezados(encabezados: &mut str) -> Vec { + let encabezados: Vec = encabezados + .trim() + .split(',') + .map(|col| col.trim().to_string()) + .collect(); + encabezados + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..89ad136 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,7 @@ +pub mod comandos; +pub mod delete; +pub mod errores; +pub mod insert; +pub mod parser; +pub mod select; +pub mod update; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..fff9789 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,42 @@ +mod comandos; +mod delete; +mod errores; +mod insert; +mod parser; +mod select; +mod update; + +use parser::parsear_consulta; +use std::env; + +fn main() { + // leo argumentos + let args: Vec = env::args().collect(); + if args.len() < 3 { + println!( + "ERROR: Por favor, proporcione una ruta a la carpeta de tablas y una consulta SQL." + ); + return; + } + + let ruta_carpeta_tablas = &args[1]; + let consulta = &args[2]; + + // Parsear la consulta + let comando = match parsear_consulta(consulta) { + Ok(result) => result, + Err(err) => { + println!("{}", err); + return; + } + }; + + match comando.ejecutar(ruta_carpeta_tablas) { + Ok(results) => { + comando.imprimir_resultados(&results); + } + Err(err) => { + println!("{}", err); + } + } +} diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..6812c6d --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,323 @@ +//! # Módulo de Parsing de Consultas SQL +//! +//! Este módulo se encarga de analizar (parsear) consultas SQL y convertirlas en estructuras de datos +//! que el programa puede manipular. Admite las operaciones básicas de SQL como `SELECT`, `INSERT`, +//! `UPDATE` y `DELETE`. +//! +//! ## Componentes +//! +//! Este módulo está compuesto por las siguientes funciones principales: +//! +//! - `parsear_consulta`: Función principal que detecta el tipo de comando SQL y delega el parsing +//! al submódulo correspondiente. +//! - `parse_select`: Analiza las consultas `SELECT` y extrae las columnas, tabla y condiciones. +//! - `parse_insert`: Analiza las consultas `INSERT` y extrae la tabla y los valores a insertar. +//! - `parse_update`: Analiza las consultas `UPDATE` y extrae las columnas a actualizar y sus valores. +//! - `parse_delete`: Analiza las consultas `DELETE` y extrae las condiciones para eliminar registros. +use crate::comandos::Comando; +use crate::delete::Delete; +use crate::errores::SqlError; +use crate::insert::Insert; +use crate::select::Select; +use crate::update::Update; + +/// Parsea una consulta SQL en una estructura `Comando` correspondiente. +/// +/// Esta función toma una consulta SQL en forma de cadena y determina su tipo (SELECT, INSERT, UPDATE, DELETE). +/// Luego, según el tipo de comando detectado, delega el parsing del resto de la consulta a la función correspondiente +/// (`parse_select`, `parse_insert`, `parse_update`, `parse_delete`). +/// +/// # Argumentos +/// +/// * `consulta` - Una cadena que contiene la consulta SQL a parsear. Se asume que las palabras clave SQL +/// (como `SELECT`, `INSERT INTO`, `UPDATE`, `DELETE FROM`) vienen en mayúsculas. +/// +/// # Retornos +/// +/// Retorna un `Result` que contiene un `Comando` si la consulta se parsea correctamente, o un `SqlError` en caso de error. +/// +/// # Errores +/// +/// Esta función retornará un `SqlError::InvalidSintax` si el comando SQL no es reconocido, o si hay algún problema +/// al intentar parsear el resto de la consulta con las funciones específicas. +pub fn parsear_consulta(consulta: &str) -> Result { + //Si pongo toda la consulta enn minusucula, poerdo las mayuduclas de las tablas, columnas y APELLIDOCS + //INTENMTE CAMBIAR LA LOGICA PERO SE ME HIZO MUUUY COMPLICADO SOLO COMPARAR ESAS PALABRAS + //ASUMO Q LAS CALVES VIENEN EN MAYSUCULA YA QUE EN LOS EJEMPLOS ESTA ASI + // let consulta_lower = consulta.to_lowercase(); + + // Determino el tipo de comando y guardar el resto de la consulta + let (comando, resto) = if let Some(resto) = consulta.strip_prefix("SELECT") { + ("select", resto.trim()) + } else if let Some(resto) = consulta.strip_prefix("INSERT INTO") { + ("insert", resto.trim()) + } else if let Some(resto) = consulta.strip_prefix("UPDATE") { + ("update", resto.trim()) + } else if let Some(resto) = consulta.strip_prefix("DELETE FROM") { + ("delete", resto.trim()) + } else { + return Err(SqlError::InvalidSintax); + }; + + // Crear el comando basado en el tipo de comando detectado + let comando = match comando { + "select" => { + let select = parse_select(resto)?; + Comando::Select(select) + } + "insert" => { + let insert = parse_insert(resto)?; + Comando::Insert(insert) + } + "update" => { + let update = parse_update(resto)?; + Comando::Update(update) + } + "delete" => { + let delete = parse_delete(resto)?; + Comando::Delete(delete) + } + _ => return Err(SqlError::InvalidSintax), + }; + Ok(comando) // Devolvemos el comando directamente, no una tupla +} + +//praseo el select +fn parse_select(resto: &str) -> Result { + let resto = resto.trim_end_matches(';').trim(); + + //Separo la parte de las columnas y la tabla usando "FROM" + let (columnas, resto) = if let Some((columnas, resto)) = resto.split_once("FROM") { + (columnas.trim(), resto.trim()) + } else { + return Err(SqlError::InvalidSintax); + }; + + //Convierto la parte de las columnas en un vector de strings + let columnas: Vec = columnas + .split(',') + .map(|columna| columna.trim().to_string()) + .collect(); + + //Separo la tabla y manejar posibles restricciones y ordenamiento + let (tabla, restricciones, ordenamiento) = separar_tabla_restricciones_ordenamiento(resto); + + Ok(Select { + columnas, + tabla, + restricciones, + ordenamiento, + }) +} + +fn separar_tabla_restricciones_ordenamiento( + resto: &str, +) -> (String, Option, Option) { + if let Some((tabla, resto)) = resto.split_once("WHERE") { + // Si hay WHERE, manejamos restricciones y ordenamiento + let (restricciones, ordenamiento) = + if let Some((restricciones, orden)) = resto.split_once("ORDER BY") { + ( + Some(restricciones.trim().to_string()), + Some(orden.trim().to_string()), + ) + } else { + (Some(resto.trim().to_string()), None) + }; + (tabla.trim().to_string(), restricciones, ordenamiento) + } else if let Some((tabla, orden)) = resto.split_once("ORDER BY") { + // Si no hay WHERE pero sí ORDER BY + ( + tabla.trim().to_string(), + None, + Some(orden.trim().to_string()), + ) + } else { + // No hay ni WHERE ni ORDER BY + (resto.trim().to_string(), None, None) + } +} + +fn parse_update(resto: &str) -> Result { + let resto = sacar_punto_y_cona_saltos_linea(resto); + + // Separo SET y WHERE + let partes: Vec<&str> = resto.splitn(2, " SET ").collect(); + if partes.len() != 2 { + return Err(SqlError::InvalidSintax); + } + + //tabla + let table_part = partes[0].trim(); + let tabla = table_part.to_string(); + + let set_where_partes: Vec<&str> = partes[1].splitn(2, " WHERE ").collect(); + let parte_set = set_where_partes[0].trim(); + + //columnas a modif y valores + let mut columnas = Vec::new(); + let mut valores = Vec::new(); + + for asignacion in parte_set.split(',') { + let mut parts = asignacion.splitn(2, '='); + + let columna = parts + .next() + .ok_or(SqlError::InvalidSintax)? + .trim() + .to_string(); + + let valor = parts + .next() + .ok_or(SqlError::InvalidSintax)? + .trim() + .to_string(); + + // Elimino comillas simples alrededor del valorrr + let valor = valor.trim_matches('\'').to_string(); + + // Verificar si la columna o valor están vacíos + if columna.is_empty() || valor.is_empty() { + return Err(SqlError::InvalidSintax); + } + + columnas.push(columna); + valores.push(valor); + } + + let restricciones = parsear_restricciones_si_hay(set_where_partes); + + Ok(Update { + columnas, + tabla, + valores, + restricciones, + }) +} + +fn parsear_restricciones_si_hay(set_where_partes: Vec<&str>) -> Option { + // Parseo restricciones, si hay + let restricciones = if set_where_partes.len() == 2 { + Some(set_where_partes[1].trim().to_string()) + } else { + None + }; + restricciones +} + +fn sacar_punto_y_cona_saltos_linea(resto: &str) -> String { + // saco ; y saltos de linea + let resto = resto.trim_end_matches(';').trim(); + let resto = resto + .replace("\n", " ") + .replace("\r", " ") + .trim() + .to_string(); + resto +} + +fn parse_insert(resto: &str) -> Result { + let resto = resto.trim(); + + let pos_values = match resto.find("VALUES") { + Some(pos) => pos, + None => { + return Err(SqlError::InvalidSintax); + } + }; + + let tabla_y_columnas = &resto[..pos_values].trim(); + + let paos_parentesis_abre = match tabla_y_columnas.find('(') { + Some(pos) => pos, + None => { + return Err(SqlError::InvalidSintax); + } + }; + + let pos_parentesis_cierra = match tabla_y_columnas.find(')') { + Some(pos) => pos, + None => { + return Err(SqlError::InvalidSintax); + } + }; + + let tabla = tabla_y_columnas[..paos_parentesis_abre].trim(); + let columnas_str = &tabla_y_columnas[paos_parentesis_abre + 1..pos_parentesis_cierra].trim(); + + let columnas = mapear_columnas(columnas_str); + + let values_str = &resto[pos_values + 6..].trim(); + let values_str = values_str.trim_end_matches(';'); + + //Divido los valores haciendo que se respeten las comillas simples + let valores = dividir_valores(values_str); + + Ok(Insert { + tabla: tabla.to_string(), + columnas, + valores, + }) +} + +fn mapear_columnas(columnas_str: &&str) -> Vec { + let columnas: Vec = columnas_str + .split(',') + .map(|col| col.trim().to_string()) + .collect(); + columnas +} + +fn dividir_valores(values_str: &str) -> Vec> { + let valores: Vec> = values_str + .split("),") + .map(|val_list| { + val_list + .trim() + .trim_start_matches('(') + .trim_end_matches(')') + .split(',') + .map(|val| { + let trimmed_val = val.trim(); + // Elimino comillas simples si hay + if trimmed_val.starts_with('\'') && trimmed_val.ends_with('\'') { + &trimmed_val[1..trimmed_val.len() - 1] + } else { + trimmed_val + } + }) + .map(|val| val.to_string()) + .collect() + }) + .collect(); + valores +} + +fn parse_delete(resto: &str) -> Result { + // saco todos los ; y saltos de linea + let resto = resto + .replace(";", "") + .replace("\n", " ") + .replace("\r", " ") + .trim() + .to_string(); + + let partes: Vec<&str> = resto.splitn(2, "WHERE").collect(); + + let tabla = partes[0].trim().to_string(); + + let restricciones = if partes.len() > 1 { + Some(partes[1].trim().replace("'", "")) + } else { + None + }; + + if tabla.is_empty() { + return Err(SqlError::InvalidTable); + } + + Ok(Delete { + tabla, + restricciones, + }) +} diff --git a/src/select.rs b/src/select.rs new file mode 100644 index 0000000..ba3712b --- /dev/null +++ b/src/select.rs @@ -0,0 +1,488 @@ +//! Módulo para gestionar la consulta `SELECT` +//! +//! El módulo incluye la estructura `Select` que representa +//! un metodo para ejecutar la consulta y otros métodos asociados para poder +//! aplicar restricciones (WHERE), y ordenar los resultados (ORDER BY). +//! +use crate::errores::SqlError; +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::iter::Peekable; +use std::str::Chars; + +/// Columnas: representa las columnas de los valores que se desean seleccionar +/// Tabla: tabla +/// Valores: Listado de registros a insertar +/// Restricciones: restricciones que determinan qué registros deben ser actualizados. (Lo +/// que viene dsps del WHERE) +/// Ordenamiento: El ordenamiento elejido +pub struct Select { + pub columnas: Vec, + pub tabla: String, + pub restricciones: Option, + pub ordenamiento: Option, +} + +impl Select { + /// Ejecuta la consulta SQL `SELECT` + /// Este método abre el archivo CSV correspondiente a la tabla especificada, + /// aplica las restricciones y el ordenamiento si están definidos, y devuelve + /// el resultado de la consulta como un vector de vectores de `String`. + /// # Retorna + /// `Result>, SqlError>` - Devuelve un vector con los resultados de la consulta si es exitosa, + /// o un error de tipo `SqlError` si ocurre algún problema durante la ejecución. + pub fn ejecutar(&self, ruta_carpeta_tablas: &str) -> Result>, SqlError> { + // Abro el archivo CSV + let ruta_archivo = format!("{}/{}.csv", ruta_carpeta_tablas, self.tabla); + let file = File::open(&ruta_archivo).map_err(|_err| SqlError::InvalidTable)?; + let mut reader = BufReader::new(file); + + // Leo la primera línea con las columnas + let mut linea_encabezados = String::new(); + let bytes_leidos = reader + .read_line(&mut linea_encabezados) + .map_err(|_err| SqlError::InvalidTable)?; + + if bytes_leidos == 0 { + //archivo vacio + return Err(SqlError::InvalidTable); + } + let vector_encabezados = Self::armar_vector_encabezados(&mut linea_encabezados); + + let mut resultado = Vec::new(); + + let mut encabezados_select = Vec::new(); + + self.agregar_encabezados(&vector_encabezados, &mut resultado, &mut encabezados_select)?; + + self.leer_linea_a_linea( + &mut reader, + &vector_encabezados, + &mut resultado, + &mut encabezados_select, + )?; + //Ordenamiento (ORDER BY) + if let Some(ref ordenamiento) = self.ordenamiento { + self.aplicar_ordenamiento(&mut resultado, ordenamiento, &encabezados_select)?; + }; + + Ok(resultado) + } + + fn leer_linea_a_linea( + &self, + reader: &mut BufReader, + vector_encabezados: &[String], + resultado: &mut Vec>, + encabezados_select: &mut Vec, + ) -> Result<(), SqlError> { + // leo linea a linea + for linea in reader.lines() { + let line = linea.map_err(|_err| SqlError::InvalidTable)?; + let registro: Vec = line.split(',').map(|s| s.to_string()).collect(); + + // Aplico restricciones (WHERE) + if self.aplicar_restricciones(®istro, vector_encabezados)? { + // Crear un vector para las columnas seleccionadas + let mut columnas_select = Vec::new(); + + for col in &mut *encabezados_select { + // Encuentro índice de la columna en los encabezados + match vector_encabezados + .iter() + .position(|encabezado| encabezado == col) + { + Some(index) => { + // Obtener el valor de la columna, si existe + if let Some(value) = registro.get(index) { + columnas_select.push(value.to_string()); + } else { + // Si el índice está fuera de rango, meto una cadena vacía + columnas_select.push(String::new()); + } + } + None => { + // Si no se encuentra el índice, meto una cadena vacía + columnas_select.push(String::new()); + } + } + } + resultado.push(columnas_select); + } + } + Ok(()) + } + + fn armar_vector_encabezados(linea_encabezados: &mut str) -> Vec { + let vector_encabezados: Vec = linea_encabezados + .trim_end() + .split(',') + .map(|s| s.to_string()) + .collect(); + vector_encabezados + } + + fn aplicar_ordenamiento( + &self, + resultado: &mut [Vec], + ordenamiento: &str, + encabezados_select: &[String], + ) -> Result<(), SqlError> { + let criterios = Self::parsear_criterios_ordenamiento(ordenamiento); + + let indice_direccion_criterios = + Self::chequear_columnas_y_mapear_indices(encabezados_select, criterios)?; + + //Ordeno las filas usando los criterios + resultado[1..].sort_by(|fila_1, fila_2| { + for &(indice_columna, ref direccion) in &indice_direccion_criterios { + let valor_1 = fila_1.get(indice_columna).map_or("", |v| v.as_str()); + let valor_2 = fila_2.get(indice_columna).map_or("", |v| v.as_str()); + + let resultado_comparacion = valor_1.cmp(valor_2); + + // Si los valores no son iguales, hago el ordenamiento + if resultado_comparacion != std::cmp::Ordering::Equal { + return if direccion == "DESC" { + resultado_comparacion.reverse() + } else { + resultado_comparacion + }; + } + } + // Si todos los criterios son iguales, no se cambia el orden + std::cmp::Ordering::Equal + }); + + Ok(()) + } + + fn chequear_columnas_y_mapear_indices( + encabezados_select: &[String], + criterios: Vec<(String, String)>, + ) -> Result, SqlError> { + //Me Aseguro que todas las columnas de los criterios existen en encabezados_select + //y mapeo los indices + let indice_direccion_criterios: Vec<(usize, String)> = criterios + .iter() + .map(|(columna, direccion)| { + if let Some(indice) = encabezados_select + .iter() + .position(|encabezado| encabezado == columna) + { + Ok((indice, direccion.to_string())) + } else { + Err(SqlError::InvalidColumn) + } + }) + .collect::, SqlError>>()?; + Ok(indice_direccion_criterios) + } + + fn parsear_criterios_ordenamiento(ordenamiento: &str) -> Vec<(String, String)> { + //Parseo los criterios de ordenamiento + let criterios: Vec<(String, String)> = ordenamiento + .split(',') + .map(|criterio| { + let (col, dir) = if let Some((col, dir)) = criterio.trim().split_once(' ') { + (col.trim().to_string(), dir.trim().to_uppercase()) + } else { + (criterio.trim().to_string(), "ASC".to_string()) + }; + (col, dir) + }) + .collect(); + criterios + } + + fn agregar_encabezados( + &self, + vector_encabezados: &[String], + resultado: &mut Vec>, + encabezados_select: &mut Vec, + ) -> Result<(), SqlError> { + if self.columnas.len() == 1 && self.columnas[0] == "*" { + // Selecciono todas si es * + encabezados_select.extend(vector_encabezados.iter().map(|s| s.as_str().to_string())); + } else { + encabezados_select.clear(); + for col in &self.columnas { + if let Some(encabezado) = vector_encabezados + .iter() + .find(|&encabezado| encabezado == col) + { + encabezados_select.push(encabezado.to_string()); + } else { + return Err(SqlError::InvalidColumn); + } + } + } + + resultado.push(Vec::with_capacity(encabezados_select.len())); + + for encabezado in encabezados_select.iter() { + resultado[0].push(encabezado.to_string()); + } + + Ok(()) + } + + pub fn aplicar_restricciones( + &self, + registro: &[String], + encabezados: &[String], + ) -> Result { + if let Some(restricciones) = &self.restricciones { + let tokens = self.tokenizar(restricciones); + let postfix = self.infix_a_postfix(&tokens)?; + let resultado = self.evaluar_postfix(&postfix, registro, encabezados)?; + Ok(resultado) + } else { + Ok(true) + } + } + + fn sacar_espacios_alrededor_operadores(&self, restricciones: &str) -> String { + let mut restricciones_limpio = String::new(); + let mut chars = restricciones.chars().peekable(); + + while let Some(ch) = chars.next() { + match ch { + '=' | '<' | '>' => { + //Eliminamos espacios antes del operador + if let Some(last_char) = restricciones_limpio.chars().last() { + if last_char == ' ' { + restricciones_limpio.pop(); + } + } + //Agregamos el operador al resultado + restricciones_limpio.push(ch); + + //Ignoramos cualquier espacio después del operador + while let Some(&next_ch) = chars.peek() { + if next_ch == ' ' { + chars.next(); + } else { + break; + } + } + } + _ => { + restricciones_limpio.push(ch); + } + } + } + + restricciones_limpio + } + + //Tokeniza las restricciones + fn tokenizar(&self, restricciones: &str) -> Vec { + let restricciones_limpio = self.sacar_espacios_alrededor_operadores(restricciones); + let mut chars = restricciones_limpio.chars().peekable(); + + let mut tokens = Vec::new(); + let mut token_actual = String::new(); + + let keywords = ["AND", "OR", "NOT", "(", ")"]; + + Self::loop_tokenizar(&mut tokens, &mut token_actual, keywords, &mut chars); + + if !token_actual.is_empty() { + tokens.push(token_actual.trim().to_string()); + } + + tokens.retain(|token| !token.is_empty()); + tokens + } + + fn loop_tokenizar( + tokens: &mut Vec, + token_actual: &mut String, + keywords: [&str; 5], + chars: &mut Peekable, + ) { + for ch in chars.by_ref() { + match ch { + ' ' if !token_actual.is_empty() => { + tokens.push(token_actual.trim().to_string()); + token_actual.clear(); + } + '(' | ')' => { + if !token_actual.is_empty() { + tokens.push(token_actual.trim().to_string()); + token_actual.clear(); + } + tokens.push(ch.to_string()); + } + _ => { + token_actual.push(ch); + let token_upper = token_actual.trim().to_uppercase(); + if keywords.contains(&token_upper.as_str()) { + tokens.push(token_upper); + token_actual.clear(); + } + } + } + } + } + + //Convierte la notación infix (con paréntesis) a notación postfija (sin parentesis) + //infix: (A OR B) AND (C OR D) ====> postfix: A B OR C D OR AND + fn infix_a_postfix(&self, tokens: &[String]) -> Result, SqlError> { + let mut resultado = Vec::new(); + let mut operadores = Vec::new(); + + let precedencia = |op: &str| match op { + "NOT" => 3, + "AND" => 2, + "OR" => 1, + "(" => 0, + ")" => 0, + _ => -1, + }; + + for token in tokens.iter() { + match token.as_str() { + "(" => operadores.push(token.to_string()), + ")" => { + while let Some(op) = operadores.pop() { + if op == "(" { + break; + } + resultado.push(op); + } + } + "AND" | "OR" | "NOT" => { + while let Some(op) = operadores.last() { + if precedencia(op) >= precedencia(token) { + // el operador en la cima de la pila se extrae de la pila + // y se coloca en el output antes de agregar el nuevo operador. + if let Some(op) = operadores.pop() { + resultado.push(op); //saco + } else { + return Err(SqlError::Error("restricciones invalidas".into())); + } + } else { + break; + } + } + operadores.push(token.to_string()); + } + _ => resultado.push(token.to_string()), + } + } + + while let Some(op) = operadores.pop() { + resultado.push(op); + } + + Ok(resultado) + } + + //Evalúo la expresión en notación postfija + fn evaluar_postfix( + &self, + tokens: &[String], + registro: &[String], + encabezados: &[String], + ) -> Result { + let mut stack = Vec::new(); + for token in tokens.iter() { + match token.as_str() { + "AND" | "OR" | "NOT" => { + let derecha = stack.pop().ok_or(SqlError::InvalidSintax)?; + let izquierda = if token != "NOT" { + stack.pop().ok_or(SqlError::InvalidSintax)? + } else { + String::new() + }; + let resultado = match token.as_str() { + "AND" => { + self.aplicar_condicion(&izquierda, registro, encabezados)? + && self.aplicar_condicion(&derecha, registro, encabezados)? + } + "OR" => { + self.aplicar_condicion(&izquierda, registro, encabezados)? + || self.aplicar_condicion(&derecha, registro, encabezados)? + } + "NOT" => !self.aplicar_condicion(&derecha, registro, encabezados)?, + _ => false, + }; + stack.push(resultado.to_string()); + } + _ => + //Proceso la condición directa + { + if stack.is_empty() && tokens.len() == 1 { + // Si el primer token es la única condición, evalúo directamente + let result = self.aplicar_condicion(token, registro, encabezados)?; + return Ok(result); + } else { + // Para otros tokens, los mete al stack para ser procesados con operadores + stack.push(token.to_string()); + } + } + } + } + //si esta vacio el stack, error, sino devuelvo el resultado (ult valor del stack) + Ok(stack.pop().ok_or(SqlError::InvalidSintax)? == "true") + } + + //Aplico condición simple (comparaciones > < = <= >=) + fn aplicar_condicion( + &self, + condicion: &str, + registro: &[String], + encabezados: &[String], + ) -> Result { + if condicion == "true" { + return Ok(true); + } else if condicion == "false" { + return Ok(false); + } + // Parseo la condición para encontrar operadores de comparación + let (columna, operador, valor) = if let Some(pos) = condicion.find(">=") { + (&condicion[..pos].trim(), ">=", &condicion[pos + 2..].trim()) + } else if let Some(pos) = condicion.find("<=") { + (&condicion[..pos].trim(), "<=", &condicion[pos + 2..].trim()) + } else if let Some(pos) = condicion.find('>') { + (&condicion[..pos].trim(), ">", &condicion[pos + 1..].trim()) + } else if let Some(pos) = condicion.find('<') { + (&condicion[..pos].trim(), "<", &condicion[pos + 1..].trim()) + } else if let Some(pos) = condicion.find('=') { + (&condicion[..pos].trim(), "=", &condicion[pos + 1..].trim()) + } else { + return Err(SqlError::Error( + "Operador no válido en la condición".to_string(), + )); + }; + + let valor = valor.trim().trim_matches('\''); + + // Encuentro el índice de la columna, o devuelvo un error si no se encuentra + if let Some(indice) = encabezados.iter().position(|enc| enc == columna) { + let valor_registro = ®istro[indice]; + let resultado = match operador { + "=" => valor_registro.as_str() == valor, + ">" => valor_registro.as_str() > valor, + "<" => valor_registro.as_str() < valor, + "<=" => valor_registro.as_str() <= valor, + ">=" => valor_registro.as_str() >= valor, + _ => false, + }; + Ok(resultado) + } else { + Err(SqlError::InvalidColumn) + } + } +} + +impl Select { + pub fn imprimir_resultados(results: &[Vec]) { + for row in results { + let row_string = row.join(","); + println!("{}", row_string); + } + } +} diff --git a/src/update.rs b/src/update.rs new file mode 100644 index 0000000..2233a23 --- /dev/null +++ b/src/update.rs @@ -0,0 +1,520 @@ +//! Módulo para gestionar el comando update +//! Este módulo proporciona una estructura y métodos para realizar la operacion update +//! Lee el archivo aplica las restricciones y utilizando un arhcivo temporal modifica +//! la tabla original +use crate::errores::SqlError; +use std::fs::File; +use std::io::{BufRead, BufReader, Lines, Write}; +use std::iter::Peekable; +use std::path::{Path, PathBuf}; +use std::str::Chars; + +/// `Update` incluye la información necesaria para identificar el archivo CSV, +/// Columans: columnas que se desean actualizar +/// Valores: los nuevos valores para esas columnas +/// Restricciones: restricciones que determinan qué registros deben ser actualizados. (Lo +/// que viene dsps del WHERE) +pub struct Update { + pub columnas: Vec, + pub tabla: String, + pub valores: Vec, + pub restricciones: Option, +} + +impl Update { + /// Ejecuta update + /// Lee el archivo CSV, aplica las restricciones, actualiza los registros que cumplen con + /// las restricciones, y guarda los cambios en el archivo. Los encabezados del archivo + /// se conservan y se escribe un archivo temporal que reemplaza al archivo original al + /// final del proceso. + /// + /// # Retorna + /// + /// * `Result>, SqlError>` - Un `Result` que contiene un `Vec` de registros actualizados + /// en caso de éxito, o un `SqlError` si ocurre algún error durante el proceso. + pub fn ejecutar(&self, ruta_carpeta_tablas: &str) -> Result>, SqlError> { + let (ruta_archivo, ruta_archivo_temporal) = self.armar_rutas(ruta_carpeta_tablas); + + let archivo_entrada = match Self::abrir_archivo_tabla(&ruta_archivo) { + Ok(value) => value, + Err(value) => return value, + }; + + let reader = BufReader::new(archivo_entrada); + + let mut archivo_temporal = match Self::crear_archivo_temporal(&ruta_archivo_temporal) { + Ok(value) => value, + Err(value) => return value, + }; + + let mut lineas = reader.lines(); + let vector_encabezados = match Self::armar_vector_encabezados(&mut lineas) { + Ok(value) => value, + Err(value) => return value, + }; + + if let Some(value) = self.chequear_columnas(&ruta_archivo_temporal, &vector_encabezados) { + return value; + } + + if let Err(_err) = writeln!(archivo_temporal, "{}", vector_encabezados.join(",")) { + return Err(SqlError::InvalidColumn); + } + + let mut filas_actualizadas = Vec::new(); + + self.lectura_linea_a_linea( + &ruta_archivo_temporal, + &mut archivo_temporal, + &mut lineas, + &vector_encabezados, + &mut filas_actualizadas, + )?; + + Self::reemplazar_orig_por_temp(&ruta_archivo, &ruta_archivo_temporal, filas_actualizadas) + } + + fn reemplazar_orig_por_temp( + ruta_archivo: &PathBuf, + ruta_archivo_temporal: &PathBuf, + filas_actualizadas: Vec>, + ) -> Result>, SqlError> { + // Reemplazo el archivo original con el archivo temporal + match std::fs::rename(ruta_archivo_temporal, ruta_archivo) { + Ok(_) => Ok(filas_actualizadas), + Err(_err) => Err(SqlError::Error( + "Error al escribir el archivo temporal".to_string(), + )), + } + } + + fn lectura_linea_a_linea( + &self, + ruta_archivo_temporal: &PathBuf, + archivo_temporal: &mut File, + lineas: &mut Lines>, + vector_encabezados: &[String], + filas_actualizadas: &mut Vec>, + ) -> Result<(), SqlError> { + for linea in lineas { + let mut registro: Vec = match linea { + Ok(line) => line.split(',').map(|s| s.trim().to_string()).collect(), + Err(_err) => { + continue; + } + }; + let registro_valido = match self.chequear_registro( + ruta_archivo_temporal, + vector_encabezados, + &mut registro, + ) { + Ok(valido) => valido, + Err(err) => { + return Err(err); + } + }; + if registro_valido { + self.modificar_registro(vector_encabezados, &mut registro); + } + + if let Some(value) = Self::escribir_registro(archivo_temporal, &mut registro) { + return value; + } + + filas_actualizadas.push(registro); + } + + Ok(()) + } + + fn modificar_registro(&self, vector_encabezados: &[String], registro: &mut [String]) { + for (columna, valor) in self.columnas.iter().zip(&self.valores) { + if let Some(indice) = vector_encabezados.iter().position(|h| h == columna) { + if indice < registro.len() { + registro[indice] = valor.to_string(); + } + } + } + } + + fn escribir_registro( + archivo_temporal: &mut File, + registro: &mut [String], + ) -> Option> { + // Escribo el registro actualizado al archivo temporal + if let Err(_err) = writeln!(archivo_temporal, "{}", registro.join(",")) { + return Some(Err(SqlError::Error( + "Error al escribir en el archivo temporal".to_string(), + ))); + } + None + } + + fn chequear_registro( + &self, + ruta_archivo_temporal: &PathBuf, + vector_encabezados: &[String], + registro: &mut [String], + ) -> Result { + // Aplico restricciones + let registro_valido = if let Some(ref _restricciones) = self.restricciones { + match self.aplicar_restricciones(registro, vector_encabezados) { + Ok(valido) => valido, + Err(err) => { + // Si `aplicar_restricciones` devuelve un error, elimino el archivo temporal + if let Err(_del_err) = std::fs::remove_file(ruta_archivo_temporal) { + return Err(SqlError::Error( + "Error al eliminar el archivo temporal".to_string(), + )); + } + // Burbujeo del error + return Err(err); + } + } + } else { + true // Si no hay restricciones, el registro es válido + }; + + Ok(registro_valido) + } + + fn chequear_columnas( + &self, + ruta_archivo_temporal: &PathBuf, + vector_encabezados: &[String], + ) -> Option>, SqlError>> { + //chequeo columnas existan en tabla + let mut encabezados_update = Vec::new(); + for col in &self.columnas { + if let Some(encabezado) = vector_encabezados + .iter() + .find(|&encabezado| encabezado == col) + { + encabezados_update.push(encabezado.to_string()); + } else { + //borro el temp + if let Err(_del_err) = std::fs::remove_file(ruta_archivo_temporal) { + return Some(Err(SqlError::Error( + "Error al eliminar el archivo temporal".to_string(), + ))); + } + return Some(Err(SqlError::InvalidColumn)); + } + } + None + } + + fn armar_vector_encabezados( + lineas: &mut Lines>, + ) -> Result, Result>, SqlError>> { + let encabezados = match lineas.next() { + Some(Ok(line)) => line, + Some(Err(_err)) => { + return Err(Err(SqlError::InvalidTable)); + } + None => { + // archivo vacío + return Err(Err(SqlError::InvalidTable)); + } + }; + + let encabezados: Vec = encabezados + .split(',') + .map(|s| s.trim().to_string()) + .collect(); + Ok(encabezados) + } + + fn abrir_archivo_tabla( + ruta_archivo: &PathBuf, + ) -> Result>, SqlError>> { + // Abro archivo tabla + let archivo_entrada = match File::open(ruta_archivo) { + Ok(file) => file, + Err(_err) => { + return Err(Err(SqlError::InvalidTable)); + } + }; + Ok(archivo_entrada) + } + + fn crear_archivo_temporal( + ruta_archivo_temporal: &PathBuf, + ) -> Result>, SqlError>> { + // Creo un archivo temporal para ir escribiendo los resultados actualizados + let archivo_temporal = match File::create(ruta_archivo_temporal) { + Ok(file) => file, + Err(_err) => { + return Err(Err(SqlError::Error( + "Error al crear el archivo temporal:".into(), + ))); + } + }; + Ok(archivo_temporal) + } + + fn armar_rutas(&self, ruta_carpeta_tablas: &str) -> (PathBuf, PathBuf) { + let ruta_archivo = Path::new(ruta_carpeta_tablas) + .join(&self.tabla) + .with_extension("csv"); + let ruta_archivo_temporal = ruta_archivo.with_extension("tmp"); + (ruta_archivo, ruta_archivo_temporal) + } + + fn aplicar_restricciones( + &self, + registro: &[String], + encabezados: &[String], + ) -> Result { + if let Some(restricciones) = &self.restricciones { + let tokens = self.tokenizar(restricciones); + let postfix = self.infix_a_postfix(&tokens)?; + let resultado = self.evaluar_postfix(&postfix, registro, encabezados)?; + Ok(resultado) + } else { + Ok(true) + } + } + + fn sacar_espacios_alrededor_operadores(&self, restricciones: &str) -> String { + let mut restricciones_limpio = String::new(); + let mut chars = restricciones.chars().peekable(); + + while let Some(ch) = chars.next() { + match ch { + '=' | '<' | '>' => { + //Eliminamos espacios antes del operador + if let Some(last_char) = restricciones_limpio.chars().last() { + if last_char == ' ' { + restricciones_limpio.pop(); + } + } + //Agregamos el operador al resultado + restricciones_limpio.push(ch); + + //Ignoramos cualquier espacio después del operador + while let Some(&next_ch) = chars.peek() { + if next_ch == ' ' { + chars.next(); + } else { + break; + } + } + } + _ => { + restricciones_limpio.push(ch); + } + } + } + + restricciones_limpio + } + + //Tokeniza las restricciones + fn tokenizar(&self, restricciones: &str) -> Vec { + let restricciones_limpio = self.sacar_espacios_alrededor_operadores(restricciones); + let mut chars = restricciones_limpio.chars().peekable(); + + let mut tokens = Vec::new(); + let mut token_actual = String::new(); + + let keywords = ["AND", "OR", "NOT", "(", ")"]; + + Self::loop_tokenizar(&mut tokens, &mut token_actual, keywords, &mut chars); + + if !token_actual.is_empty() { + tokens.push(token_actual.trim().to_string()); + } + + tokens.retain(|token| !token.is_empty()); + + tokens + } + + fn loop_tokenizar( + tokens: &mut Vec, + token_actual: &mut String, + keywords: [&str; 5], + chars: &mut Peekable, + ) { + for ch in chars.by_ref() { + match ch { + ' ' if !token_actual.is_empty() => { + tokens.push(token_actual.trim().to_string()); + token_actual.clear(); + } + '(' | ')' => { + if !token_actual.is_empty() { + tokens.push(token_actual.trim().to_string()); + token_actual.clear(); + } + tokens.push(ch.to_string()); + } + _ => { + token_actual.push(ch); + let token_upper = token_actual.trim().to_uppercase(); + if keywords.contains(&token_upper.as_str()) { + tokens.push(token_upper); + token_actual.clear(); + } + } + } + } + } + + //Convierte la notación infix (con paréntesis) a notación postfija (sin parentesis) + //infix: (A OR B) AND (C OR D) ====> postfix: A B OR C D OR AND + fn infix_a_postfix(&self, tokens: &[String]) -> Result, SqlError> { + let mut resultado = Vec::new(); + let mut operadores = Vec::new(); + + let precedencia = |op: &str| match op { + "NOT" => 3, + "AND" => 2, + "OR" => 1, + "(" => 0, + ")" => 0, + _ => -1, + }; + + for token in tokens.iter() { + match token.as_str() { + "(" => operadores.push(token.to_string()), + ")" => { + while let Some(op) = operadores.pop() { + if op == "(" { + break; + } + resultado.push(op); + } + } + "AND" | "OR" | "NOT" => { + while let Some(op) = operadores.last() { + if precedencia(op) >= precedencia(token) { + // el operador en la cima de la pila se extrae de la pila + // y se coloca en el output antes de agregar el nuevo operador. + if let Some(op) = operadores.pop() { + resultado.push(op); //saco + } else { + return Err(SqlError::InvalidSintax); + } + } else { + break; + } + } + operadores.push(token.to_string()); + } + _ => resultado.push(token.to_string()), + } + } + + while let Some(op) = operadores.pop() { + resultado.push(op); + } + + Ok(resultado) + } + + //Evalúo la expresión en notación postfija + fn evaluar_postfix( + &self, + tokens: &[String], + registro: &[String], + encabezados: &[String], + ) -> Result { + let mut stack = Vec::new(); + + for token in tokens.iter() { + match token.as_str() { + "AND" | "OR" | "NOT" => { + let derecha = stack.pop().ok_or(SqlError::InvalidSintax)?; + let izquierda = if token != "NOT" { + stack.pop().ok_or(SqlError::InvalidSintax)? + } else { + String::new() + }; + let resultado = match token.as_str() { + "AND" => { + self.aplicar_condicion(&izquierda, registro, encabezados)? + && self.aplicar_condicion(&derecha, registro, encabezados)? + } + "OR" => { + self.aplicar_condicion(&izquierda, registro, encabezados)? + || self.aplicar_condicion(&derecha, registro, encabezados)? + } + "NOT" => !self.aplicar_condicion(&derecha, registro, encabezados)?, + _ => false, + }; + stack.push(resultado.to_string()); + } + _ => + //Proceso la condición directa + { + if stack.is_empty() && tokens.len() == 1 { + // Si el primer token es la única condición, evalúo directamente + let result = self.aplicar_condicion(token, registro, encabezados)?; + return Ok(result); + } else { + // Para otros tokens, los mete al stack para ser procesados con operadores + stack.push(token.to_string()); + } + } + } + } + //si esta vacio el stack error, sino devuelvo el resultado (ult valor del stack) + Ok(stack + .pop() + .ok_or(SqlError::Error("expresión invalida".into()))? + == "true") + } + + //Aplico condición simple (comparaciones > < =) + fn aplicar_condicion( + &self, + condicion: &str, + registro: &[String], + encabezados: &[String], + ) -> Result { + if condicion == "true" { + return Ok(true); + } else if condicion == "false" { + return Ok(false); + } + + // Parseo la condición para encontrar operadores de comparación + let (columna, operador, valor) = if let Some(pos) = condicion.find(">=") { + (&condicion[..pos].trim(), ">=", &condicion[pos + 2..].trim()) + } else if let Some(pos) = condicion.find("<=") { + (&condicion[..pos].trim(), "<=", &condicion[pos + 2..].trim()) + } else if let Some(pos) = condicion.find('>') { + (&condicion[..pos].trim(), ">", &condicion[pos + 1..].trim()) + } else if let Some(pos) = condicion.find('<') { + (&condicion[..pos].trim(), "<", &condicion[pos + 1..].trim()) + } else if let Some(pos) = condicion.find('=') { + (&condicion[..pos].trim(), "=", &condicion[pos + 1..].trim()) + } else { + return Err(SqlError::Error( + "Operador no válido en la condición".to_string(), + )); + }; + + let valor = valor.trim().trim_matches('\''); + + // Encuentro el índice de la columna, o devuelvo un error si no se encuentra + if let Some(indice) = encabezados.iter().position(|enc| enc == columna) { + let valor_registro = ®istro[indice]; + let resultado = match operador { + "=" => valor_registro.as_str() == valor, + ">" => valor_registro.as_str() > valor, + "<" => valor_registro.as_str() < valor, + "<=" => valor_registro.as_str() <= valor, + ">=" => valor_registro.as_str() >= valor, + _ => false, + }; + + Ok(resultado) + } else { + Err(SqlError::InvalidColumn) + } + } +} diff --git a/tablas/clientes.csv b/tablas/clientes.csv new file mode 100644 index 0000000..85310a6 --- /dev/null +++ b/tablas/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/tablas/clientes_limpio.csv b/tablas/clientes_limpio.csv new file mode 100644 index 0000000..85310a6 --- /dev/null +++ b/tablas/clientes_limpio.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/tablas/ordenes.csv b/tablas/ordenes.csv new file mode 100644 index 0000000..e8d32be --- /dev/null +++ b/tablas/ordenes.csv @@ -0,0 +1,12 @@ +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 +111,6,Laptop,3 diff --git a/tablas/ordenes_limpio.csv b/tablas/ordenes_limpio.csv new file mode 100644 index 0000000..bf38946 --- /dev/null +++ b/tablas/ordenes_limpio.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/delete_test.rs b/tests/delete_test.rs new file mode 100644 index 0000000..6392e3b --- /dev/null +++ b/tests/delete_test.rs @@ -0,0 +1,110 @@ +extern crate tp_individual_taller_9508; + +use std::fs; +use std::fs::File; +use std::io::Write; +use tp_individual_taller_9508::comandos::Comando; +use tp_individual_taller_9508::delete::Delete; + +#[test] +fn test_comando_delete_con_and_not() { + let ruta_carpeta = "./test_data"; + let ruta_archivo = format!("{}/clientes.csv", ruta_carpeta); + + fs::create_dir_all(ruta_carpeta).unwrap(); + + let mut file = File::create(&ruta_archivo).unwrap(); + writeln!(file, "id,nombre,apellido,email").unwrap(); + writeln!(file, "1,Juan,Pérez,juan.perez@email.com").unwrap(); + writeln!(file, "2,Ana,López,ana.lopez@email.com").unwrap(); + writeln!(file, "3,Carlos,Gómez,carlos.gomez@email.com").unwrap(); + writeln!(file, "4,María,Rodríguez,maria.rodriguez@email.com").unwrap(); + writeln!(file, "5,José,López,jose.lopez@email.com").unwrap(); + writeln!(file, "6,Laura,Fernández,laura.fernandez@email.com").unwrap(); + + let comando_delete = Comando::Delete(Delete { + tabla: "clientes".to_string(), + restricciones: Some("id > 1 AND NOT nombre = 'Ana'".to_string()), + }); + + let resultado = comando_delete.ejecutar(ruta_carpeta); + + assert!(resultado.is_ok()); + + let contenido_actualizado = fs::read_to_string(&ruta_archivo).unwrap(); + let esperado = "id,nombre,apellido,email\n1,Juan,Pérez,juan.perez@email.com\n2,Ana,López,ana.lopez@email.com\n"; + assert_eq!(contenido_actualizado, esperado); + + fs::remove_file(&ruta_archivo).unwrap(); + fs::remove_dir_all(ruta_carpeta).unwrap(); +} +#[test] +fn test_comando_delete_parentesis_or_and_or() { + let ruta_carpeta = "./test_data_2"; + let ruta_archivo = format!("{}/clientes.csv", ruta_carpeta); + + fs::create_dir_all(ruta_carpeta).unwrap(); + + let mut file = File::create(&ruta_archivo).unwrap(); + writeln!(file, "id,nombre,apellido,email").unwrap(); + writeln!(file, "1,Juan,Pérez,juan.perez@email.com").unwrap(); + writeln!(file, "2,Ana,López,ana.lopez@email.com").unwrap(); + writeln!(file, "3,Carlos,Gómez,carlos.gomez@email.com").unwrap(); + writeln!(file, "4,María,Rodríguez,maria.rodriguez@email.com").unwrap(); + writeln!(file, "5,José,López,jose.lopez@email.com").unwrap(); + writeln!(file, "6,Laura,Fernández,laura.fernandez@email.com").unwrap(); + + let comando_delete = Comando::Delete(Delete { + tabla: "clientes".to_string(), + restricciones: Some("(id=2 OR apellido='López') AND (nombre='Ana' OR id = 5)".to_string()), + }); + + let resultado = comando_delete.ejecutar(ruta_carpeta); + + assert!(resultado.is_ok()); + + let contenido_actualizado = fs::read_to_string(&ruta_archivo).unwrap(); + let esperado = r#"id,nombre,apellido,email +1,Juan,Pérez,juan.perez@email.com +3,Carlos,Gómez,carlos.gomez@email.com +4,María,Rodríguez,maria.rodriguez@email.com +6,Laura,Fernández,laura.fernandez@email.com +"#; + assert_eq!(contenido_actualizado, esperado); + + fs::remove_file(&ruta_archivo).unwrap(); + fs::remove_dir_all(ruta_carpeta).unwrap(); +} + +#[test] +fn test_comando_delete_sin_where_borra_todo_menos_encabezados() { + let ruta_carpeta = "./test_data_3"; + let ruta_archivo = format!("{}/clientes.csv", ruta_carpeta); + + fs::create_dir_all(ruta_carpeta).unwrap(); + + let mut file = File::create(&ruta_archivo).unwrap(); + writeln!(file, "id,nombre,apellido,email").unwrap(); + writeln!(file, "1,Juan,Pérez,juan.perez@email.com").unwrap(); + writeln!(file, "2,Ana,López,ana.lopez@email.com").unwrap(); + writeln!(file, "3,Carlos,Gómez,carlos.gomez@email.com").unwrap(); + writeln!(file, "4,María,Rodríguez,maria.rodriguez@email.com").unwrap(); + writeln!(file, "5,José,López,jose.lopez@email.com").unwrap(); + writeln!(file, "6,Laura,Fernández,laura.fernandez@email.com").unwrap(); + + let comando_delete = Comando::Delete(Delete { + tabla: "clientes".to_string(), + restricciones: None, + }); + + let resultado = comando_delete.ejecutar(ruta_carpeta); + + assert!(resultado.is_ok()); + + let contenido_actualizado = fs::read_to_string(&ruta_archivo).unwrap(); + let esperado = "id,nombre,apellido,email\n"; + assert_eq!(contenido_actualizado, esperado); + + fs::remove_file(&ruta_archivo).unwrap(); + fs::remove_dir_all(ruta_carpeta).unwrap(); +} diff --git a/tests/insert_tests.rs b/tests/insert_tests.rs new file mode 100644 index 0000000..0c43a3c --- /dev/null +++ b/tests/insert_tests.rs @@ -0,0 +1,141 @@ +extern crate tp_individual_taller_9508; + +use std::fs; +use std::fs::File; +use std::io::Write; +use tp_individual_taller_9508::comandos::Comando; +use tp_individual_taller_9508::insert::Insert; + +#[test] +fn test_insert_una_fila_con_cols_y_valores_ok() { + let ruta_carpeta = "./insert_test1_data"; + let ruta_archivo = format!("{}/ordenes.csv", ruta_carpeta); + + fs::create_dir_all(ruta_carpeta).unwrap(); + + let mut file = File::create(&ruta_archivo).unwrap(); + writeln!(file, "id,id_cliente,producto,cantidad").unwrap(); + writeln!(file, "101,1,Laptop,1").unwrap(); + writeln!(file, "103,1,Monitor,1").unwrap(); + writeln!(file, "102,2,Teléfono,2").unwrap(); + writeln!(file, "104,3,Teclado,1").unwrap(); + writeln!(file, "105,4,Mouse,2").unwrap(); + writeln!(file, "106,5,Impresora,1").unwrap(); + writeln!(file, "107,6,Altavoces,1").unwrap(); + writeln!(file, "108,4,Auriculares,1").unwrap(); + writeln!(file, "109,5,Laptop,1").unwrap(); + writeln!(file, "110,6,Teléfono,2").unwrap(); + + let comando_delete = Comando::Insert(Insert { + tabla: "ordenes".to_string(), + columnas: vec![ + "id".to_string(), + "id_cliente".to_string(), + "producto".to_string(), + "cantidad".to_string(), + ], + valores: vec![vec![ + "111".to_string(), + "6".to_string(), + "Laptop".to_string(), + "3".to_string(), + ]], + }); + + let resultado = comando_delete.ejecutar(ruta_carpeta); + + assert!(resultado.is_ok()); + + let contenido_actualizado = fs::read_to_string(&ruta_archivo).unwrap(); + let esperado = "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 +111,6,Laptop,3\n"; + assert_eq!(contenido_actualizado, esperado); + + fs::remove_file(&ruta_archivo).unwrap(); + fs::remove_dir_all(ruta_carpeta).unwrap(); +} + +#[test] +fn test_insert_3_filas_con_cols_y_valores_ok() { + let ruta_carpeta = "./insert_test2_data"; + let ruta_archivo = format!("{}/ordenes.csv", ruta_carpeta); + + fs::create_dir_all(ruta_carpeta).unwrap(); + + let mut file = File::create(&ruta_archivo).unwrap(); + writeln!(file, "id,id_cliente,producto,cantidad").unwrap(); + writeln!(file, "101,1,Laptop,1").unwrap(); + writeln!(file, "103,1,Monitor,1").unwrap(); + writeln!(file, "102,2,Teléfono,2").unwrap(); + writeln!(file, "104,3,Teclado,1").unwrap(); + writeln!(file, "105,4,Mouse,2").unwrap(); + writeln!(file, "106,5,Impresora,1").unwrap(); + writeln!(file, "107,6,Altavoces,1").unwrap(); + writeln!(file, "108,4,Auriculares,1").unwrap(); + writeln!(file, "109,5,Laptop,1").unwrap(); + writeln!(file, "110,6,Teléfono,2").unwrap(); + + let comando_insert = Comando::Insert(Insert { + tabla: "ordenes".to_string(), + columnas: vec![ + "id".to_string(), + "id_cliente".to_string(), + "producto".to_string(), + "cantidad".to_string(), + ], + valores: vec![ + vec![ + "111".to_string(), + "1".to_string(), + "Laptop1".to_string(), + "1".to_string(), + ], + vec![ + "112".to_string(), + "1".to_string(), + "Laptop2".to_string(), + "1".to_string(), + ], + vec![ + "113".to_string(), + "1".to_string(), + "Laptop3".to_string(), + "1".to_string(), + ], + ], + }); + + let resultado = comando_insert.ejecutar(ruta_carpeta); + + assert!(resultado.is_ok()); + + let contenido_actualizado = fs::read_to_string(&ruta_archivo).unwrap(); + let esperado = "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 +111,1,Laptop1,1 +112,1,Laptop2,1 +113,1,Laptop3,1\n"; + assert_eq!(contenido_actualizado, esperado); + + fs::remove_file(&ruta_archivo).unwrap(); + fs::remove_dir_all(ruta_carpeta).unwrap(); +} diff --git a/tests/select_test.rs b/tests/select_test.rs new file mode 100644 index 0000000..42fd61f --- /dev/null +++ b/tests/select_test.rs @@ -0,0 +1,92 @@ +extern crate tp_individual_taller_9508; + +use std::fs; +use std::fs::File; +use std::io::Write; +use tp_individual_taller_9508::comandos::Comando; +use tp_individual_taller_9508::select::Select; + +#[test] +fn select_3de4_cols_1_restriccion_simple_con_mayor_ordenamiento_por1_col_desc() { + let ruta_carpeta = "./select_test_1"; + let ruta_archivo = format!("{}/clientes.csv", ruta_carpeta); + + fs::create_dir_all(ruta_carpeta).unwrap(); + let mut file = File::create(&ruta_archivo).unwrap(); + writeln!(file, "id,nombre,apellido,email").unwrap(); + writeln!(file, "1,Juan,Pérez,juan.perez@email.com").unwrap(); + writeln!(file, "2,Ana,López,ana.lopez@email.com").unwrap(); + writeln!(file, "3,Carlos,Gómez,carlos.gomez@email.com").unwrap(); + writeln!(file, "4,María,Rodríguez,maria.rodriguez@email.com").unwrap(); + writeln!(file, "5,José,López,jose.lopez@email.com").unwrap(); + writeln!(file, "6,Laura,Fernández,laura.fernandez@email.com").unwrap(); + + let comando_select = Comando::Select(Select { + columnas: vec!["id".to_string(), "nombre".to_string(), "email".to_string()], + tabla: "clientes".to_string(), + restricciones: Some("apellido = 'López'".to_string()), + ordenamiento: Some("email DESC".to_string()), + }); + + let resultado = comando_select.ejecutar(ruta_carpeta).unwrap(); + + let mut resultado_string = String::new(); + for row in resultado { + let line = row.join(","); + resultado_string.push_str(&line); + resultado_string.push('\n'); + } + + let salida_esperada = + "id,nombre,email\n5,José,jose.lopez@email.com\n2,Ana,ana.lopez@email.com\n"; + + assert_eq!(resultado_string, salida_esperada); + + fs::remove_file(&ruta_archivo).unwrap(); + fs::remove_dir_all(ruta_carpeta).unwrap(); +} +#[test] +fn select_todo_sin_restricciones_ordenamiento_por_2columnas() { + let ruta_carpeta = "./select_test_2"; + let ruta_archivo = format!("{}/clientes.csv", ruta_carpeta); + + // Crear el archivo de prueba + fs::create_dir_all(ruta_carpeta).unwrap(); + let mut file = File::create(&ruta_archivo).unwrap(); + writeln!(file, "id,nombre,apellido,email").unwrap(); + writeln!(file, "1,Juan,Pérez,juan.perez@email.com").unwrap(); + writeln!(file, "2,Ana,López,ana.lopez@email.com").unwrap(); + writeln!(file, "3,Carlos,Gómez,carlos.gomez@email.com").unwrap(); + writeln!(file, "4,María,Rodríguez,maria.rodriguez@email.com").unwrap(); + writeln!(file, "5,José,López,jose.lopez@email.com").unwrap(); + writeln!(file, "6,Laura,Fernández,laura.fernandez@email.com").unwrap(); + + let comando_select = Comando::Select(Select { + columnas: vec!["*".to_string()], + tabla: "clientes".to_string(), + restricciones: None, + ordenamiento: Some("apellido, nombre DESC".to_string()), + }); + + let resultado = comando_select.ejecutar(ruta_carpeta).unwrap(); + + let mut resultado_string = String::new(); + for row in resultado { + let line = row.join(","); + resultado_string.push_str(&line); + resultado_string.push('\n'); + } + + let salida_esperada = "id,nombre,apellido,email +6,Laura,Fernández,laura.fernandez@email.com +3,Carlos,Gómez,carlos.gomez@email.com +5,José,López,jose.lopez@email.com +2,Ana,López,ana.lopez@email.com +1,Juan,Pérez,juan.perez@email.com +4,María,Rodríguez,maria.rodriguez@email.com\n"; + + assert_eq!(resultado_string, salida_esperada); + + fs::remove_file(&ruta_archivo).unwrap(); + fs::remove_dir_all(ruta_carpeta).unwrap(); +} diff --git a/tests/tests_integracion.rs b/tests/tests_integracion.rs new file mode 100644 index 0000000..cc5b61c --- /dev/null +++ b/tests/tests_integracion.rs @@ -0,0 +1,230 @@ +use std::fs; +use std::fs::File; +use std::io::Write; +use std::process::Command; + +#[test] +fn select_si_tabla_no_existe_devuelve_invalid_table() { + let ruta_carpeta = "./tests_integracion_1"; + let archivo_tablas = format!("{}/clientes.csv", ruta_carpeta); + + fs::create_dir_all(ruta_carpeta).unwrap(); + + let _file = File::create(&archivo_tablas).unwrap(); + + let mut file = File::create(&archivo_tablas).unwrap(); + writeln!(file, "id,nombre,apellido,email").unwrap(); + writeln!(file, "1,Juan,Pérez,juan.perez@email.com").unwrap(); + + let consulta = "SELECT * FROM clientesssss;"; + + let output = Command::new("cargo") + .args(&["run", "--", &ruta_carpeta, consulta]) + .output() + .expect("INVALID_TABLE: Ocurrió un error al procesar la tabla."); + + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + + assert!(stdout.contains("INVALID_TABLE: Ocurrió un error al procesar la tabla.")); + + fs::remove_file(archivo_tablas).unwrap(); + fs::remove_dir_all(ruta_carpeta).unwrap(); +} + +#[test] +fn select_si_no_existe_columna_en_order_by_devuelve_invalidcolumn() { + let ruta_carpeta = "./tests_integracion_2"; + let archivo_tablas = format!("{}/clientes.csv", ruta_carpeta); + + fs::create_dir_all(ruta_carpeta).unwrap(); + + let _file = File::create(&archivo_tablas).unwrap(); + + let mut file = File::create(&archivo_tablas).unwrap(); + writeln!(file, "id,nombre,apellido,email").unwrap(); + writeln!(file, "1,Juan,Pérez,juan.perez@email.com").unwrap(); + + let consulta = "SELECT * FROM clientes ORDER BY nombressss;"; + + let output = Command::new("cargo") + .args(&["run", "--", &ruta_carpeta, consulta]) + .output() + .expect("INVALID_COLUMN: Ocurrió un error al procesar alguna columna."); + + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + + assert!(stdout.contains("INVALID_COLUMN: Ocurrió un error al procesar alguna columna.")); + + fs::remove_file(archivo_tablas).unwrap(); + fs::remove_dir_all(ruta_carpeta).unwrap(); +} + +#[test] +fn ejemplo_1_consigna() { + let ruta_carpeta = "./tests_integracion_3"; + let archivo_tablas = format!("{}/ordenes.csv", ruta_carpeta); + + fs::create_dir_all(ruta_carpeta).unwrap(); + + let _file = File::create(&archivo_tablas).unwrap(); + + let mut file = File::create(&archivo_tablas).unwrap(); + writeln!(file, "id,id_cliente,producto,cantidad").unwrap(); + writeln!(file, "101,1,Laptop,1").unwrap(); + writeln!(file, "103,1,Monitor,1").unwrap(); + writeln!(file, "102,2,Teléfono,2").unwrap(); + writeln!(file, "104,3,Teclado,1").unwrap(); + writeln!(file, "105,4,Mouse,2").unwrap(); + writeln!(file, "106,5,Impresora,1").unwrap(); + writeln!(file, "107,6,Altavoces,1").unwrap(); + writeln!(file, "108,4,Auriculares,1").unwrap(); + writeln!(file, "109,5,Laptop,1").unwrap(); + writeln!(file, "110,6,Teléfono,2").unwrap(); + + let consulta = "SELECT id, producto, id_cliente +FROM ordenes +WHERE cantidad > 1; +"; + + let output = Command::new("cargo") + .args(&["run", "--", &ruta_carpeta, consulta]) + .output() + .expect("un output"); + + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + + assert!( + stdout.contains("id,producto,id_cliente\n102,Teléfono,2\n105,Mouse,4\n110,Teléfono,6\n") + ); + + fs::remove_file(archivo_tablas).unwrap(); + fs::remove_dir_all(ruta_carpeta).unwrap(); +} + +#[test] +fn ejemplo_2_consigna() { + let ruta_carpeta = "./tests_integracion_4"; + let archivo_tablas = format!("{}/clientes.csv", ruta_carpeta); + + fs::create_dir_all(ruta_carpeta).unwrap(); + + let _file = File::create(&archivo_tablas).unwrap(); + + let mut file = File::create(&archivo_tablas).unwrap(); + writeln!(file, "id,nombre,apellido,email").unwrap(); + writeln!(file, "1,Juan,Pérez,juan.perez@email.com").unwrap(); + writeln!(file, "2,Ana,López,ana.lopez@email.com").unwrap(); + writeln!(file, "3,Carlos,Gómez,carlos.gomez@email.com").unwrap(); + writeln!(file, "4,María,Rodríguez,maria.rodriguez@email.com").unwrap(); + writeln!(file, "5,José,López,jose.lopez@email.com").unwrap(); + writeln!(file, "6,Laura,Fernández,laura.fernandez@email.com").unwrap(); + + let consulta = "SELECT id, nombre, email +FROM clientes +WHERE apellido = 'López' +ORDER BY email DESC; +"; + + let output = Command::new("cargo") + .args(&["run", "--", &ruta_carpeta, consulta]) + .output() + .expect("un output"); + + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + + assert!(stdout + .contains("id,nombre,email\n5,José,jose.lopez@email.com\n2,Ana,ana.lopez@email.com\n")); + + fs::remove_file(archivo_tablas).unwrap(); + fs::remove_dir_all(ruta_carpeta).unwrap(); +} + +#[test] +fn ejemplo_3_consigna() { + let ruta_carpeta = "./tests_integracion_5"; + let ruta_archivo = format!("{}/clientes.csv", ruta_carpeta); + + fs::create_dir_all(ruta_carpeta).unwrap(); + + let mut file = File::create(&ruta_archivo).unwrap(); + writeln!(file, "id,nombre,apellido,email").unwrap(); + writeln!(file, "1,Juan,Pérez,juan.perez@email.com").unwrap(); + writeln!(file, "2,Ana,López,ana.lopez@email.com").unwrap(); + writeln!(file, "3,Carlos,Gómez,carlos.gomez@email.com").unwrap(); + writeln!(file, "4,María,Rodríguez,maria.rodriguez@email.com").unwrap(); + writeln!(file, "5,José,López,jose.lopez@email.com").unwrap(); + writeln!(file, "6,Laura,Fernández,laura.fernandez@email.com").unwrap(); + + let consulta = "UPDATE clientes +SET email = 'mrodriguez@hotmail.com' +WHERE id = 4;"; + + let _output = Command::new("cargo") + .args(&["run", "--", &ruta_carpeta, consulta]) + .output() + .expect("un output"); + + let contenido_actualizado = fs::read_to_string(&ruta_archivo).unwrap(); + let esperado = "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,mrodriguez@hotmail.com +5,José,López,jose.lopez@email.com +6,Laura,Fernández,laura.fernandez@email.com\n"; + assert_eq!(contenido_actualizado, esperado); + + fs::remove_file(&ruta_archivo).unwrap(); + fs::remove_dir_all(ruta_carpeta).unwrap(); +} + +#[test] +fn ejemplo_4_consigna() { + let ruta_carpeta = "./tests_integracion_6"; + let ruta_archivo = format!("{}/ordenes.csv", ruta_carpeta); + + fs::create_dir_all(ruta_carpeta).unwrap(); + + let mut file = File::create(&ruta_archivo).unwrap(); + writeln!(file, "id,id_cliente,producto,cantidad").unwrap(); + writeln!(file, "101,1,Laptop,1").unwrap(); + writeln!(file, "103,1,Monitor,1").unwrap(); + writeln!(file, "102,2,Teléfono,2").unwrap(); + writeln!(file, "104,3,Teclado,1").unwrap(); + writeln!(file, "105,4,Mouse,2").unwrap(); + writeln!(file, "106,5,Impresora,1").unwrap(); + writeln!(file, "107,6,Altavoces,1").unwrap(); + writeln!(file, "108,4,Auriculares,1").unwrap(); + writeln!(file, "109,5,Laptop,1").unwrap(); + writeln!(file, "110,6,Teléfono,2").unwrap(); + + let consulta = + "INSERT INTO ordenes (id, id_cliente, producto, cantidad) VALUES (111, 6, 'Laptop', 3);"; + + let _output = Command::new("cargo") + .args(&["run", "--", &ruta_carpeta, consulta]) + .output() + .expect("un output"); + + let contenido_actualizado = fs::read_to_string(&ruta_archivo).unwrap(); + let esperado = "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 +111,6,Laptop,3\n"; + assert_eq!(contenido_actualizado, esperado); + + fs::remove_file(&ruta_archivo).unwrap(); + fs::remove_dir_all(ruta_carpeta).unwrap(); +} diff --git a/tests/update_test.rs b/tests/update_test.rs new file mode 100644 index 0000000..d381c48 --- /dev/null +++ b/tests/update_test.rs @@ -0,0 +1,52 @@ +extern crate tp_individual_taller_9508; + +use std::fs; +use std::fs::File; +use std::io::Write; +use tp_individual_taller_9508::comandos::Comando; +use tp_individual_taller_9508::update::Update; + +#[test] +fn test_comando_update_set_tres_campos() { + let ruta_carpeta = "./test_data_3"; + let ruta_archivo = format!("{}/clientes.csv", ruta_carpeta); + + fs::create_dir_all(ruta_carpeta).unwrap(); + + let mut file = File::create(&ruta_archivo).unwrap(); + writeln!(file, "id,nombre,apellido,email").unwrap(); + writeln!(file, "1,Juan,Pérez,juan.perez@email.com").unwrap(); + writeln!(file, "2,Ana,López,ana.lopez@email.com").unwrap(); + writeln!(file, "3,Carlos,Gómez,carlos.gomez@email.com").unwrap(); + writeln!(file, "4,María,Rodríguez,maria.rodriguez@email.com").unwrap(); + writeln!(file, "5,José,López,jose.lopez@email.com").unwrap(); + writeln!(file, "6,Laura,Fernández,laura.fernandez@email.com").unwrap(); + + let comando_update = Comando::Update(Update { + columnas: vec!["email".to_string(), "nombre".to_string(), "id".to_string()], + tabla: "clientes".to_string(), + valores: vec![ + "mrodriguez@hotmail.com".to_string(), + "Mariiia".to_string(), + "7".to_string(), + ], + restricciones: Some("id = 4".to_string()), + }); + + let resultado = comando_update.ejecutar(ruta_carpeta); + + assert!(resultado.is_ok()); + + let contenido_actualizado = fs::read_to_string(&ruta_archivo).unwrap(); + let esperado = "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 +7,Mariiia,Rodríguez,mrodriguez@hotmail.com +5,José,López,jose.lopez@email.com +6,Laura,Fernández,laura.fernandez@email.com\n"; + assert_eq!(contenido_actualizado, esperado); + + fs::remove_file(&ruta_archivo).unwrap(); + fs::remove_dir_all(ruta_carpeta).unwrap(); +}