From 65506902bdbb20e1dbb9c76465c2bc19742e1a68 Mon Sep 17 00:00:00 2001 From: Fletcher555 Date: Mon, 8 Sep 2025 21:59:05 -0400 Subject: [PATCH 1/4] Add required tokens to tokenizer --- src/interpreter/tokenizer/mod.rs | 15 +++++++++++++++ src/interpreter/tokenizer/scanner.rs | 5 +++++ src/interpreter/tokenizer/token.rs | 3 ++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/interpreter/tokenizer/mod.rs b/src/interpreter/tokenizer/mod.rs index 34cb02c..4b6ae9c 100644 --- a/src/interpreter/tokenizer/mod.rs +++ b/src/interpreter/tokenizer/mod.rs @@ -216,4 +216,19 @@ mod tests { ]; assert_eq!(expected, result); } + + #[test] + fn tokenizer_parses_more_keywords() { + let result = tokenize("ALTER RENAME TO COLUMN ADD DROP"); + let expected = vec![ + token(TokenTypes::Alter, "ALTER", 0, 1), + token(TokenTypes::Rename, "RENAME", 6, 1), + token(TokenTypes::To, "TO", 13, 1), + token(TokenTypes::Column, "COLUMN", 16, 1), + token(TokenTypes::Add, "ADD", 23, 1), + token(TokenTypes::Drop, "DROP", 27, 1), + token(TokenTypes::EOF, "", 0, 0), + ]; + assert_eq!(expected, result); + } } \ No newline at end of file diff --git a/src/interpreter/tokenizer/scanner.rs b/src/interpreter/tokenizer/scanner.rs index beb4f78..8372e6e 100644 --- a/src/interpreter/tokenizer/scanner.rs +++ b/src/interpreter/tokenizer/scanner.rs @@ -126,9 +126,14 @@ impl<'a> Scanner<'a> { slice if slice.eq_ignore_ascii_case("WHERE") => TokenTypes::Where, slice if slice.eq_ignore_ascii_case("UPDATE") => TokenTypes::Update, slice if slice.eq_ignore_ascii_case("DELETE") => TokenTypes::Delete, + slice if slice.eq_ignore_ascii_case("ADD") => TokenTypes::Add, slice if slice.eq_ignore_ascii_case("DROP") => TokenTypes::Drop, slice if slice.eq_ignore_ascii_case("INDEX") => TokenTypes::Index, slice if slice.eq_ignore_ascii_case("SET") => TokenTypes::Set, + slice if slice.eq_ignore_ascii_case("ALTER") => TokenTypes::Alter, + slice if slice.eq_ignore_ascii_case("RENAME") => TokenTypes::Rename, + slice if slice.eq_ignore_ascii_case("TO") => TokenTypes::To, + slice if slice.eq_ignore_ascii_case("COLUMN") => TokenTypes::Column, slice if slice.eq_ignore_ascii_case("INTEGER") => TokenTypes::Integer, slice if slice.eq_ignore_ascii_case("REAL") => TokenTypes::Real, slice if slice.eq_ignore_ascii_case("TEXT") => TokenTypes::Text, diff --git a/src/interpreter/tokenizer/token.rs b/src/interpreter/tokenizer/token.rs index b59d72e..84ea62f 100644 --- a/src/interpreter/tokenizer/token.rs +++ b/src/interpreter/tokenizer/token.rs @@ -2,7 +2,8 @@ pub enum TokenTypes { // Keywords Create, Select, Insert, Table, From, Into, Values, Where, - Update, Delete, Drop, Index, Set, + Update, Delete, Add, Drop, Index, Set, Alter, Rename, + To, Column, // Data Types Integer, Real, Text, Blob, Null, // Constraints From c9a8f4802658ee59982ce2f48bc511f1e5fd277e Mon Sep 17 00:00:00 2001 From: Fletcher555 Date: Mon, 8 Sep 2025 22:18:42 -0400 Subject: [PATCH 2/4] Implement AST for ALTER TABLE statements --- src/db/database.rs | 10 +- src/interpreter/ast/alter_table_statement.rs | 183 +++++++++++++++++++ src/interpreter/ast/create_statement.rs | 18 +- src/interpreter/ast/helpers/common.rs | 15 +- src/interpreter/ast/mod.rs | 21 +++ src/interpreter/ast/parser.rs | 5 + 6 files changed, 235 insertions(+), 17 deletions(-) create mode 100644 src/interpreter/ast/alter_table_statement.rs diff --git a/src/db/database.rs b/src/db/database.rs index e3720df..ee25e7d 100644 --- a/src/db/database.rs +++ b/src/db/database.rs @@ -1,5 +1,5 @@ use crate::db::table::{drop_table, Table, Value}; -use crate::interpreter::ast::{CreateTableStatement, DeleteStatement, DropTableStatement, InsertIntoStatement, SelectStatementStack, SqlStatement, UpdateStatement}; +use crate::interpreter::ast::{CreateTableStatement, DeleteStatement, DropTableStatement, InsertIntoStatement, SelectStatementStack, SqlStatement, UpdateStatement, AlterTableStatement}; use crate::db::table::select; use crate::db::table::insert; use crate::db::table::delete; @@ -44,6 +44,10 @@ impl Database { self.drop_table(statement)?; Ok(None) } + SqlStatement::AlterTable(statement) => { + self.alter_table(statement)?; + Ok(None) + } } } @@ -74,6 +78,10 @@ impl Database { drop_table::drop_table(self, statement) } + fn alter_table(&mut self, _statement: AlterTableStatement) -> Result<(), String> { + todo!(); + } + pub fn has_table(&self, table_name: &str) -> bool { self.tables.contains_key(table_name) } diff --git a/src/interpreter/ast/alter_table_statement.rs b/src/interpreter/ast/alter_table_statement.rs new file mode 100644 index 0000000..0ca0704 --- /dev/null +++ b/src/interpreter/ast/alter_table_statement.rs @@ -0,0 +1,183 @@ +use crate::interpreter::{ + ast::{parser::Parser, SqlStatement, AlterTableStatement, AlterTableAction}, + ast::helpers::common::{expect_token_type, get_table_name, token_to_data_type}, + tokenizer::token::TokenTypes, +}; +use crate::db::table::ColumnDefinition; + +pub fn build(parser: &mut Parser) -> Result { + parser.advance()?; + expect_token_type(parser, TokenTypes::Table)?; + parser.advance()?; + let table_name = get_table_name(parser)?; + let action = get_action(parser)?; + return Ok(SqlStatement::AlterTable(AlterTableStatement { + table_name: table_name, + action: action, + })); +} + +fn get_action(parser: &mut Parser) -> Result { + return match parser.current_token()?.token_type { + TokenTypes::Rename => { + parser.advance()?; + let action = match parser.current_token()?.token_type { + TokenTypes::Column => { + parser.advance()?; + expect_token_type(parser, TokenTypes::Identifier)?; + let old_column_name = parser.current_token()?.value.to_string(); + parser.advance()?; + expect_token_type(parser, TokenTypes::To)?; + parser.advance()?; + expect_token_type(parser, TokenTypes::Identifier)?; + let new_column_name = parser.current_token()?.value.to_string(); + AlterTableAction::RenameColumn { old_column_name, new_column_name } + } + TokenTypes::To => { + parser.advance()?; + expect_token_type(parser, TokenTypes::Identifier)?; + let new_table_name = parser.current_token()?.value.to_string(); + AlterTableAction::RenameTable { new_table_name } + } + _ => return Err(parser.format_error()), + }; + Ok(action) + }, + TokenTypes::Add => { + parser.advance()?; + expect_token_type(parser, TokenTypes::Column)?; + parser.advance()?; + expect_token_type(parser, TokenTypes::Identifier)?; + let name = parser.current_token()?.value.to_string(); + parser.advance()?; + let data_type = token_to_data_type(parser)?; + Ok(AlterTableAction::AddColumn { column_def: ColumnDefinition { name, data_type, constraints: vec![] } }) + } + TokenTypes::Drop => { + parser.advance()?; + expect_token_type(parser, TokenTypes::Column)?; + parser.advance()?; + expect_token_type(parser, TokenTypes::Identifier)?; + let column_name = parser.current_token()?.value.to_string(); + Ok(AlterTableAction::DropColumn { column_name }) + } + _ => return Err(parser.format_error()), + } +} + + + +#[cfg(test)] +mod tests { + use super::*; + use crate::interpreter::ast::test_utils::token; + use crate::db::table::DataType; + + #[test] + fn alter_table_statement_with_all_tokens_is_generated_correctly() { + // ALTER TABLE users RENAME COLUMN name TO new_name; + let tokens = vec![ + token(TokenTypes::Alter, "ALTER"), + token(TokenTypes::Table, "TABLE"), + token(TokenTypes::Identifier, "users"), + token(TokenTypes::Rename, "RENAME"), + token(TokenTypes::Column, "COLUMN"), + token(TokenTypes::Identifier, "name"), + token(TokenTypes::To, "TO"), + token(TokenTypes::Identifier, "new_name"), + token(TokenTypes::SemiColon, ";"), + ]; + let mut parser = Parser::new(tokens); + let result = build(&mut parser); + assert!(result.is_ok()); + let statement = result.unwrap(); + let expected = SqlStatement::AlterTable(AlterTableStatement { + table_name: "users".to_string(), + action: AlterTableAction::RenameColumn { + old_column_name: "name".to_string(), + new_column_name: "new_name".to_string() + }, + }); + assert_eq!(expected, statement); + } + + #[test] + fn alter_table_statement_with_rename_table_is_generated_correctly() { + // ALTER TABLE users RENAME TO new_name; + let tokens = vec![ + token(TokenTypes::Alter, "ALTER"), + token(TokenTypes::Table, "TABLE"), + token(TokenTypes::Identifier, "users"), + token(TokenTypes::Rename, "RENAME"), + token(TokenTypes::To, "TO"), + token(TokenTypes::Identifier, "new_name"), + token(TokenTypes::SemiColon, ";"), + ]; + let mut parser = Parser::new(tokens); + let result = build(&mut parser); + assert!(result.is_ok()); + let statement = result.unwrap(); + let expected = SqlStatement::AlterTable(AlterTableStatement { + table_name: "users".to_string(), + action: AlterTableAction::RenameTable { + new_table_name: "new_name".to_string() + }, + }); + assert_eq!(expected, statement); + } + + #[test] + fn alter_table_statement_with_add_column_is_generated_correctly() { + // ALTER TABLE users ADD COLUMN name BLOB; + let tokens = vec![ + token(TokenTypes::Alter, "ALTER"), + token(TokenTypes::Table, "TABLE"), + token(TokenTypes::Identifier, "users"), + token(TokenTypes::Add, "ADD"), + token(TokenTypes::Column, "COLUMN"), + token(TokenTypes::Identifier, "name"), + token(TokenTypes::Blob, "BLOB"), + token(TokenTypes::SemiColon, ";"), + ]; + let mut parser = Parser::new(tokens); + let result = build(&mut parser); + assert!(result.is_ok()); + let statement = result.unwrap(); + let expected = SqlStatement::AlterTable(AlterTableStatement { + table_name: "users".to_string(), + action: AlterTableAction::AddColumn { + column_def: ColumnDefinition { + name: "name".to_string(), + data_type: DataType::Blob, + constraints: vec![] + } + }, + }); + assert_eq!(expected, statement); + } + + #[test] + fn alter_table_statement_with_drop_column_is_generated_correctly() { + // ALTER TABLE users DROP COLUMN name; + let tokens = vec![ + token(TokenTypes::Alter, "ALTER"), + token(TokenTypes::Table, "TABLE"), + token(TokenTypes::Identifier, "users"), + token(TokenTypes::Drop, "DROP"), + token(TokenTypes::Column, "COLUMN"), + token(TokenTypes::Identifier, "name"), + token(TokenTypes::SemiColon, ";"), + ]; + let mut parser = Parser::new(tokens); + let result = build(&mut parser); + assert!(result.is_ok()); + let statement = result.unwrap(); + let expected = SqlStatement::AlterTable(AlterTableStatement { + table_name: "users".to_string(), + action: AlterTableAction::DropColumn { + column_name: "name".to_string() + }, + }); + assert_eq!(expected, statement); + } +} \ No newline at end of file diff --git a/src/interpreter/ast/create_statement.rs b/src/interpreter/ast/create_statement.rs index 58679e2..d78b475 100644 --- a/src/interpreter/ast/create_statement.rs +++ b/src/interpreter/ast/create_statement.rs @@ -1,11 +1,11 @@ use crate::interpreter::{ ast::{ parser::Parser, CreateTableStatement, SqlStatement::{self, CreateTable}, ExistenceCheck, - helpers::common::{expect_token_type, get_table_name, exists_clause} + helpers::common::{expect_token_type, get_table_name, exists_clause, token_to_data_type} }, tokenizer::token::TokenTypes }; -use crate::db::table::{ColumnDefinition, DataType}; +use crate::db::table::ColumnDefinition; pub fn build(parser: &mut Parser) -> Result { parser.advance()?; @@ -84,24 +84,12 @@ fn column_definitions(parser: &mut Parser) -> Result, Stri return Ok(columns); } -fn token_to_data_type(parser: &mut Parser) -> Result { - let token = parser.current_token()?; - return match token.token_type { - TokenTypes::Integer => Ok(DataType::Integer), - TokenTypes::Real => Ok(DataType::Real), - TokenTypes::Text => Ok(DataType::Text), - TokenTypes::Blob => Ok(DataType::Blob), - TokenTypes::Null => Ok(DataType::Null), - _ => Err(parser.format_error()), - }; -} - - #[cfg(test)] mod tests { use super::*; use crate::interpreter::ast::test_utils::token; use crate::interpreter::ast::ExistenceCheck; + use crate::db::table::DataType; #[test] fn create_table_generates_proper_statement(){ diff --git a/src/interpreter/ast/helpers/common.rs b/src/interpreter/ast/helpers/common.rs index 6f05be9..d5aa172 100644 --- a/src/interpreter/ast/helpers/common.rs +++ b/src/interpreter/ast/helpers/common.rs @@ -1,6 +1,6 @@ use crate::interpreter::{ast::{parser::Parser, ExistenceCheck}, tokenizer::token::TokenTypes}; -use crate::db::table::Value; +use crate::db::table::{Value, DataType}; // Returns an error if the current token does not match the given token type pub fn expect_token_type(parser: &Parser, token_type: TokenTypes) -> Result<(), String> { @@ -109,6 +109,19 @@ fn decode(hex: &str) -> Result, String> { }).collect() } +pub fn token_to_data_type(parser: &mut Parser) -> Result { + let token = parser.current_token()?; + return match token.token_type { + TokenTypes::Integer => Ok(DataType::Integer), + TokenTypes::Real => Ok(DataType::Real), + TokenTypes::Text => Ok(DataType::Text), + TokenTypes::Blob => Ok(DataType::Blob), + TokenTypes::Null => Ok(DataType::Null), + _ => Err(parser.format_error()), + }; +} + + #[cfg(test)] mod tests { use super::*; diff --git a/src/interpreter/ast/mod.rs b/src/interpreter/ast/mod.rs index 209a3b1..69b17cc 100644 --- a/src/interpreter/ast/mod.rs +++ b/src/interpreter/ast/mod.rs @@ -9,6 +9,7 @@ mod update_statement; mod delete_statement; mod helpers; mod drop_statement; +mod alter_table_statement; #[cfg(test)] mod test_utils; @@ -27,6 +28,7 @@ pub enum SqlStatement { UpdateStatement(UpdateStatement), DeleteStatement(DeleteStatement), DropTable(DropTableStatement), + AlterTable(AlterTableStatement), } #[derive(Debug, PartialEq)] @@ -119,6 +121,20 @@ pub struct UpdateStatement { pub limit_clause: Option, } +#[derive(Debug, PartialEq)] +pub struct AlterTableStatement { + pub table_name: String, + pub action: AlterTableAction, +} + +#[derive(Debug, PartialEq)] +pub enum AlterTableAction { + RenameTable { new_table_name: String }, + RenameColumn { old_column_name: String, new_column_name: String }, + AddColumn { column_def: ColumnDefinition }, + DropColumn { column_name: String }, +} + #[derive(Debug, PartialEq)] pub struct ColumnValue { pub column: String, @@ -235,6 +251,7 @@ pub trait StatementBuilder { fn build_update(&self, parser: &mut parser::Parser) -> Result; fn build_delete(&self, parser: &mut parser::Parser) -> Result; fn build_drop(&self, parser: &mut parser::Parser) -> Result; + fn build_alter(&self, parser: &mut parser::Parser) -> Result; } pub struct DefaultStatementBuilder; @@ -263,6 +280,10 @@ impl StatementBuilder for DefaultStatementBuilder { fn build_drop(&self, parser: &mut parser::Parser) -> Result { drop_statement::build(parser) } + + fn build_alter(&self, parser: &mut parser::Parser) -> Result { + alter_table_statement::build(parser) + } } pub fn generate(tokens: Vec) -> Vec> { diff --git a/src/interpreter/ast/parser.rs b/src/interpreter/ast/parser.rs index 4af8fb9..38cf953 100644 --- a/src/interpreter/ast/parser.rs +++ b/src/interpreter/ast/parser.rs @@ -87,6 +87,7 @@ impl<'a> Parser<'a> { (TokenTypes::Update, _) => Some(builder.build_update(self)), (TokenTypes::Delete, _) => Some(builder.build_delete(self)), (TokenTypes::Drop, _) => Some(builder.build_drop(self)), + (TokenTypes::Alter, _) => Some(builder.build_alter(self)), _ => { Some(Err(self.format_error())) } @@ -174,6 +175,10 @@ mod tests { fn build_drop(&self, _parser: &mut Parser) -> Result { todo!(); } + + fn build_alter(&self, _parser: &mut Parser) -> Result { + todo!(); + } } #[test] From 758866286f1070bd29a64c446e6a06a5665b4947 Mon Sep 17 00:00:00 2001 From: Fletcher555 Date: Mon, 8 Sep 2025 22:45:29 -0400 Subject: [PATCH 3/4] Implement ALTER TABLE in DB --- src/db/database.rs | 7 +- src/db/table/alter_table/mod.rs | 135 ++++++++++++++++++++++++++++++++ src/db/table/mod.rs | 1 + 3 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 src/db/table/alter_table/mod.rs diff --git a/src/db/database.rs b/src/db/database.rs index ee25e7d..3fcf5d0 100644 --- a/src/db/database.rs +++ b/src/db/database.rs @@ -5,6 +5,7 @@ use crate::db::table::insert; use crate::db::table::delete; use crate::db::table::update; use crate::db::table::create_table; +use crate::db::table::alter_table; use std::collections::HashMap; pub struct Database { @@ -78,8 +79,8 @@ impl Database { drop_table::drop_table(self, statement) } - fn alter_table(&mut self, _statement: AlterTableStatement) -> Result<(), String> { - todo!(); + fn alter_table(&mut self, statement: AlterTableStatement) -> Result<(), String> { + alter_table::alter_table(self, statement) } pub fn has_table(&self, table_name: &str) -> bool { @@ -93,7 +94,7 @@ impl Database { Ok(self.tables.get(table_name).unwrap()) } - fn get_table_mut(&mut self, table_name: &str) -> Result<&mut Table, String> { + pub fn get_table_mut(&mut self, table_name: &str) -> Result<&mut Table, String> { if !self.has_table(table_name) { return Err(format!("Table not found: {}", table_name)); } diff --git a/src/db/table/alter_table/mod.rs b/src/db/table/alter_table/mod.rs new file mode 100644 index 0000000..19a9924 --- /dev/null +++ b/src/db/table/alter_table/mod.rs @@ -0,0 +1,135 @@ +use crate::db::database::Database; +use crate::interpreter::ast::{AlterTableStatement, AlterTableAction}; +use crate::db::table::Value; + + +pub fn alter_table(database: &mut Database, statement: AlterTableStatement) -> Result<(), String> { + return match statement.action { + AlterTableAction::RenameTable { new_table_name } => { + let table = database.tables.remove(&statement.table_name); + match table { + Some(table) => database.tables.insert(new_table_name, table), + None => return Err(format!("Table {} does not exist", statement.table_name)), + }; + Ok(()) + } + AlterTableAction::RenameColumn { old_column_name, new_column_name } => { + let table = database.get_table_mut(&statement.table_name)?; + if !table.has_column(&old_column_name){ + return Err(format!("Column {} does not exist in table {}", old_column_name, statement.table_name)); + } + table.columns.iter_mut().for_each(|column| { + if column.name == old_column_name { + column.name = new_column_name.clone(); + } + }); + Ok(()) + } + AlterTableAction::AddColumn { column_def } => { + let table = database.get_table_mut(&statement.table_name)?; + if table.has_column(&column_def.name){ + return Err(format!("Column {} already exists in table {}", column_def.name, statement.table_name)); + } + table.columns.push(column_def); + table.rows.iter_mut().for_each(|row| { + row.push(Value::Null); + }); + Ok(()) + } + AlterTableAction::DropColumn { column_name } => { + let table = database.get_table_mut(&statement.table_name)?; + if !table.has_column(&column_name){ + return Err(format!("Column {} does not exist in table {}", column_name, statement.table_name)); + } + let index = table.get_index_of_column(&column_name)?; + // This is kind of bad because it's an O(n^2) operation however SQLite + // preserves the order of the columns after drop column statements. + table.columns.remove(index); + table.rows.iter_mut().for_each(|row| { + row.remove(index); + }); + Ok(()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::table::test_utils::default_database; + use crate::db::table::{ColumnDefinition, DataType}; + + #[test] + fn alter_table_rename_table_works_correctly() { + let mut database = default_database(); + let statement = AlterTableStatement { + table_name: "users".to_string(), + action: AlterTableAction::RenameTable { new_table_name: "new_users".to_string() }, + }; + let result = alter_table(&mut database, statement); + assert!(result.is_ok()); + assert!(!database.tables.contains_key("users")); + assert!(database.tables.contains_key("new_users")); + } + + #[test] + fn alter_table_rename_column_works_correctly() { + let mut database = default_database(); + let statement = AlterTableStatement { + table_name: "users".to_string(), + action: AlterTableAction::RenameColumn { old_column_name: "name".to_string(), new_column_name: "new_name".to_string() }, + }; + let result = alter_table(&mut database, statement); + assert!(result.is_ok()); + let table = database.get_table("users"); + assert!(table.is_ok()); + assert!(table.unwrap().columns.iter().any(|column| column.name == "new_name")); + } + + #[test] + fn alter_table_add_column_works_correctly() { + let mut database = default_database(); + let statement = AlterTableStatement { + table_name: "users".to_string(), + action: AlterTableAction::AddColumn { column_def: ColumnDefinition { name: "new_column".to_string(), data_type: DataType::Integer, constraints: vec![] } }, + }; + let result = alter_table(&mut database, statement); + assert!(result.is_ok()); + let table = database.get_table("users"); + assert!(table.is_ok()); + let table = table.unwrap(); + assert!(table.columns.last().unwrap().name == "new_column"); + assert!(table.columns.len() == table.rows.first().unwrap().len()); + assert!(table.rows.iter().all(|row| row.last().unwrap() == &Value::Null)); + } + + #[test] + fn alter_table_drop_column_works_correctly() { + let mut database = default_database(); + let statement = AlterTableStatement { + table_name: "users".to_string(), + action: AlterTableAction::DropColumn { column_name: "age".to_string() }, + }; + let result = alter_table(&mut database, statement); + assert!(result.is_ok()); + let table = database.get_table("users"); + assert!(table.is_ok()); + let table = table.unwrap(); + assert!(!table.columns.iter().any(|column| column.name == "age")); + assert!(table.columns.len() == table.rows.first().unwrap().len()); + let expected_columns_in_order = + vec![ + ColumnDefinition {name: "id".to_string(), data_type: DataType::Integer, constraints: vec![]}, + ColumnDefinition {name: "name".to_string(), data_type: DataType::Text, constraints: vec![]}, + ColumnDefinition {name: "money".to_string(), data_type: DataType::Real, constraints: vec![]}, + ]; + assert_eq!( expected_columns_in_order, table.columns); + let expected_rows = vec![ + vec![Value::Integer(1), Value::Text("John".to_string()), Value::Real(1000.0)], + vec![Value::Integer(2), Value::Text("Jane".to_string()), Value::Real(2000.0)], + vec![Value::Integer(3), Value::Text("Jim".to_string()), Value::Real(3000.0)], + vec![Value::Integer(4), Value::Null, Value::Real(4000.0)], + ]; + assert_eq!(expected_rows, table.rows); + } +} \ No newline at end of file diff --git a/src/db/table/mod.rs b/src/db/table/mod.rs index 9714568..be82630 100644 --- a/src/db/table/mod.rs +++ b/src/db/table/mod.rs @@ -10,6 +10,7 @@ pub mod update; pub mod create_table; pub mod helpers; pub mod drop_table; +pub mod alter_table; #[cfg(test)] pub mod test_utils; From f3a3117390db65fbb22cc344c445e96a65e21c7f Mon Sep 17 00:00:00 2001 From: Fletcher555 Date: Mon, 8 Sep 2025 23:01:54 -0400 Subject: [PATCH 4/4] Integration tests for ALTER TABLE statement --- src/db/database.rs | 12 ++--- src/db/table/alter_table/mod.rs | 8 ++-- src/db/table/drop_table/mod.rs | 4 +- src/interpreter/ast/alter_table_statement.rs | 2 + tests/crud_test.rs | 47 ++++++++++++++++++-- 5 files changed, 58 insertions(+), 15 deletions(-) diff --git a/src/db/database.rs b/src/db/database.rs index 3fcf5d0..44c747a 100644 --- a/src/db/database.rs +++ b/src/db/database.rs @@ -89,14 +89,14 @@ impl Database { pub fn get_table(&self, table_name: &str) -> Result<&Table, String> { if !self.has_table(table_name) { - return Err(format!("Table not found: {}", table_name)); + return Err(format!("Table `{}` does not exist", table_name)); } Ok(self.tables.get(table_name).unwrap()) } pub fn get_table_mut(&mut self, table_name: &str) -> Result<&mut Table, String> { if !self.has_table(table_name) { - return Err(format!("Table not found: {}", table_name)); + return Err(format!("Table `{}` does not exist", table_name)); } Ok(self.tables.get_mut(table_name).unwrap()) } @@ -139,15 +139,15 @@ mod tests { let mut database = default_database(); let table = database.get_table("users"); assert!(table.is_ok()); - assert_eq!(table.unwrap().name, "users"); + assert_eq!("users", table.unwrap().name); let table = database.get_table("not_users"); assert!(table.is_err()); - assert_eq!(table.unwrap_err(), "Table not found: not_users"); + assert_eq!("Table `not_users` does not exist",table.unwrap_err()); let table = database.get_table_mut("users"); assert!(table.is_ok()); - assert_eq!(table.unwrap().name, "users"); + assert_eq!("users", table.unwrap().name); let table = database.get_table_mut("not_users"); assert!(table.is_err()); - assert_eq!(table.unwrap_err(), "Table not found: not_users"); + assert_eq!("Table `not_users` does not exist", table.unwrap_err()); } } \ No newline at end of file diff --git a/src/db/table/alter_table/mod.rs b/src/db/table/alter_table/mod.rs index 19a9924..78b45ad 100644 --- a/src/db/table/alter_table/mod.rs +++ b/src/db/table/alter_table/mod.rs @@ -9,14 +9,14 @@ pub fn alter_table(database: &mut Database, statement: AlterTableStatement) -> R let table = database.tables.remove(&statement.table_name); match table { Some(table) => database.tables.insert(new_table_name, table), - None => return Err(format!("Table {} does not exist", statement.table_name)), + None => return Err(format!("Table `{}` does not exist", statement.table_name)), }; Ok(()) } AlterTableAction::RenameColumn { old_column_name, new_column_name } => { let table = database.get_table_mut(&statement.table_name)?; if !table.has_column(&old_column_name){ - return Err(format!("Column {} does not exist in table {}", old_column_name, statement.table_name)); + return Err(format!("Column `{}` does not exist in table `{}`", old_column_name, statement.table_name)); } table.columns.iter_mut().for_each(|column| { if column.name == old_column_name { @@ -28,7 +28,7 @@ pub fn alter_table(database: &mut Database, statement: AlterTableStatement) -> R AlterTableAction::AddColumn { column_def } => { let table = database.get_table_mut(&statement.table_name)?; if table.has_column(&column_def.name){ - return Err(format!("Column {} already exists in table {}", column_def.name, statement.table_name)); + return Err(format!("Column `{}` already exists in table `{}`", column_def.name, statement.table_name)); } table.columns.push(column_def); table.rows.iter_mut().for_each(|row| { @@ -39,7 +39,7 @@ pub fn alter_table(database: &mut Database, statement: AlterTableStatement) -> R AlterTableAction::DropColumn { column_name } => { let table = database.get_table_mut(&statement.table_name)?; if !table.has_column(&column_name){ - return Err(format!("Column {} does not exist in table {}", column_name, statement.table_name)); + return Err(format!("Column `{}` does not exist in table `{}`", column_name, statement.table_name)); } let index = table.get_index_of_column(&column_name)?; // This is kind of bad because it's an O(n^2) operation however SQLite diff --git a/src/db/table/drop_table/mod.rs b/src/db/table/drop_table/mod.rs index 48f7947..fad5f31 100644 --- a/src/db/table/drop_table/mod.rs +++ b/src/db/table/drop_table/mod.rs @@ -8,7 +8,7 @@ pub fn drop_table(database: &mut Database, statement: DropTableStatement) -> Res return Ok(()); } _ => { - return Err(format!("Table {} does not exist", statement.table_name)); + return Err(format!("Table `{}` does not exist", statement.table_name)); } } } @@ -43,7 +43,7 @@ mod tests { let mut database = Database::new(); let result = drop_table(&mut database, statement); assert!(result.is_err()); - assert_eq!("Table users does not exist", result.err().unwrap()); + assert_eq!("Table `users` does not exist", result.err().unwrap()); } #[test] diff --git a/src/interpreter/ast/alter_table_statement.rs b/src/interpreter/ast/alter_table_statement.rs index 0ca0704..9f39cbd 100644 --- a/src/interpreter/ast/alter_table_statement.rs +++ b/src/interpreter/ast/alter_table_statement.rs @@ -11,6 +11,8 @@ pub fn build(parser: &mut Parser) -> Result { parser.advance()?; let table_name = get_table_name(parser)?; let action = get_action(parser)?; + parser.advance()?; + expect_token_type(parser, TokenTypes::SemiColon)?; return Ok(SqlStatement::AlterTable(AlterTableStatement { table_name: table_name, action: action, diff --git a/tests/crud_test.rs b/tests/crud_test.rs index b1427b0..fd0039b 100644 --- a/tests/crud_test.rs +++ b/tests/crud_test.rs @@ -97,7 +97,7 @@ fn test_execution_errors() { "; let result = run_sql(&mut database, sql); assert!(result.iter().all(|result| result.is_err())); - let expected = vec![Err("Execution Error with statement starting on line 2 \n Error: Table not found: users".to_string())]; + let expected = vec![Err("Execution Error with statement starting on line 2 \n Error: Table `users` does not exist".to_string())]; assert_eq!(expected, result); } @@ -120,10 +120,51 @@ fn test_drop_table() { assert!(result[1].is_ok() && result[1].as_ref().unwrap().is_none()); assert!(result[2].is_ok() && result[2].as_ref().unwrap().is_none()); assert!(result[3].is_err()); - let expected_first = "Execution Error with statement starting on line 8 \n Error: Table users does not exist"; + let expected_first = "Execution Error with statement starting on line 8 \n Error: Table `users` does not exist"; assert_eq!(expected_first, result[3].as_ref().err().unwrap()); assert!(result[4].is_err()); - let expected_second = "Execution Error with statement starting on line 9 \n Error: Table not found: users"; + let expected_second = "Execution Error with statement starting on line 9 \n Error: Table `users` does not exist"; assert_eq!(expected_second, result[4].as_ref().err().unwrap()); +} + +#[test] +fn test_alter_table() { + let mut database = Database::new(); + let sql = " + CREATE TABLE users ( + id INTEGER, + name TEXT + ); + INSERT INTO users (id, name) VALUES (1, 'John'); + ALTER TABLE users RENAME TO new_users; + ALTER TABLE new_users RENAME COLUMN name TO new_name; + ALTER TABLE new_users ADD COLUMN new_column INTEGER; + ALTER TABLE new_users DROP COLUMN id; + SELECT * FROM new_users; + + ALTER TABLE users RENAME TO new_users; + ALTER TABLE new_users DROP COLUMN id; + ALTER TABLE new_users ADD COLUMN new_column INTEGER; + ALTER TABLE new_users RENAME COLUMN id TO id_new; + "; + let result = run_sql(&mut database, sql); + + assert!(result[0..=5].iter().all(|result| result.is_ok() && result.as_ref().unwrap().is_none())); + + let expected = vec![ + vec![Value::Text("John".to_string()), Value::Null], + ]; + let row = result[6].as_ref().unwrap().as_ref().unwrap(); + assert_eq!(expected, *row); + + let expected_errors = vec![ + "Execution Error with statement starting on line 13 \n Error: Table `users` does not exist", + "Execution Error with statement starting on line 14 \n Error: Column `id` does not exist in table `new_users`", + "Execution Error with statement starting on line 15 \n Error: Column `new_column` already exists in table `new_users`", + "Execution Error with statement starting on line 16 \n Error: Column `id` does not exist in table `new_users`", + ]; + + assert!(result[7..=10].iter().all(|result| result.is_err())); + assert_eq!(expected_errors, result[7..=10].iter().map(|result| result.as_ref().err().unwrap()).collect::>()); } \ No newline at end of file