From 04100075d12ea4ecf10e341ca8d5e8cb32452b5f Mon Sep 17 00:00:00 2001 From: Fletcher555 Date: Thu, 18 Sep 2025 14:41:53 -0400 Subject: [PATCH 1/4] Support CREATE TABLE with transactions + add integration test --- src/db/transactions/rollback.rs | 3 +++ tests/transaction_test.rs | 27 ++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/db/transactions/rollback.rs b/src/db/transactions/rollback.rs index 7277b33..4546c88 100644 --- a/src/db/transactions/rollback.rs +++ b/src/db/transactions/rollback.rs @@ -33,6 +33,9 @@ pub fn rollback_transaction_entry( } }, SqlStatement::Select(_) => {} // These should be kept in the log but obv do nothing. + SqlStatement::CreateTable(_) => { + database.tables.remove(statement.table_name.as_str()); + }, _ => return Err("UNSUPPORTED".to_string()), } return Ok(()); diff --git a/tests/transaction_test.rs b/tests/transaction_test.rs index 2eef1e3..9c76ab3 100644 --- a/tests/transaction_test.rs +++ b/tests/transaction_test.rs @@ -29,7 +29,6 @@ fn test_transaction() { SELECT * FROM new_users; "; let result = run_sql(&mut database, sql); - println!("{:?}", result); let expected = vec![ Ok(None), Ok(None), @@ -62,3 +61,29 @@ fn test_transaction() { assert_eq!(expected[i], *result); } } + +#[test] +fn test_transaction_create_table() { + let mut database = Database::new(); + let sql = " + BEGIN; + CREATE TABLE users ( + id INTEGER, + name TEXT + ); + SELECT * FROM users; + ROLLBACK; + SELECT * FROM users; + "; + let result = run_sql(&mut database, sql); + let expected = vec![ + Ok(None), + Ok(None), + Ok(Some(vec![])), + Ok(None), + Err("Execution Error with statement starting on line 9 \n Error: Table `users` does not exist".to_string()), + ]; + for (i, result) in result.iter().enumerate() { + assert_eq!(expected[i], *result); + } +} From b61e05ac173e662e256bc508d29c2b60e9b2c658 Mon Sep 17 00:00:00 2001 From: Fletcher555 Date: Thu, 18 Sep 2025 14:46:53 -0400 Subject: [PATCH 2/4] Make tables act as stacks as well so that we can handle dropping one --- src/db/database.rs | 10 +++++----- src/db/table/operations/alter_table/mod.rs | 7 ++++--- src/db/table/operations/create_table/mod.rs | 2 +- src/db/table/test_utils.rs | 2 +- src/db/transactions/rollback.rs | 4 ++-- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/db/database.rs b/src/db/database.rs index 5329690..82e7d50 100644 --- a/src/db/database.rs +++ b/src/db/database.rs @@ -8,7 +8,7 @@ use crate::interpreter::ast::SqlStatement; use std::collections::HashMap; pub struct Database { - pub tables: HashMap, + pub tables: HashMap>, pub transaction: TransactionLog, } @@ -120,14 +120,14 @@ impl Database { if !self.has_table(table_name) { return Err(format!("Table `{}` does not exist", table_name)); } - Ok(self.tables.get(table_name).unwrap()) + Ok(self.tables.get(table_name).unwrap().last().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 `{}` does not exist", table_name)); } - Ok(self.tables.get_mut(table_name).unwrap()) + Ok(self.tables.get_mut(table_name).unwrap().last_mut().unwrap()) } } @@ -140,7 +140,7 @@ mod tests { Database { tables: HashMap::from([( "users".to_string(), - Table::new( + vec![Table::new( "users".to_string(), vec![ ColumnDefinition { @@ -154,7 +154,7 @@ mod tests { constraints: vec![], }, ], - ), + )], )]), transaction: TransactionLog { entries: None }, } diff --git a/src/db/table/operations/alter_table/mod.rs b/src/db/table/operations/alter_table/mod.rs index ee9fa98..95dddfc 100644 --- a/src/db/table/operations/alter_table/mod.rs +++ b/src/db/table/operations/alter_table/mod.rs @@ -11,9 +11,10 @@ pub fn alter_table( AlterTableAction::RenameTable { new_table_name } => { let table = database.tables.remove(&statement.table_name); match table { - Some(mut table) => { + Some(mut table_stack) => { + let table = table_stack.last_mut().unwrap(); table.change_name(new_table_name, is_transaction); - database.tables.insert(table.name()?.clone(), table); + database.tables.insert(table.name()?.clone(), table_stack); } None => return Err(format!("Table `{}` does not exist", statement.table_name)), }; @@ -115,7 +116,7 @@ mod tests { assert!(result.is_ok()); assert!(!database.tables.contains_key("users")); assert!(database.tables.contains_key("new_users")); - assert!(database.tables.get("new_users").unwrap().name().unwrap() == "new_users"); + assert!(database.tables.get("new_users").unwrap().last().unwrap().name().unwrap() == "new_users"); } #[test] diff --git a/src/db/table/operations/create_table/mod.rs b/src/db/table/operations/create_table/mod.rs index 8336be9..c999022 100644 --- a/src/db/table/operations/create_table/mod.rs +++ b/src/db/table/operations/create_table/mod.rs @@ -17,7 +17,7 @@ pub fn create_table( } } let table = Table::new(statement.table_name, statement.columns); - database.tables.insert(table.name()?.clone(), table); + database.tables.insert(table.name()?.clone(), vec![table]); Ok(()) } diff --git a/src/db/table/test_utils.rs b/src/db/table/test_utils.rs index bf06133..38038bb 100644 --- a/src/db/table/test_utils.rs +++ b/src/db/table/test_utils.rs @@ -70,7 +70,7 @@ pub fn default_table() -> Table { #[cfg(test)] pub fn default_database() -> Database { let mut database = Database::new(); - database.tables.insert("users".to_string(), default_table()); + database.tables.insert("users".to_string(), vec![default_table()]); database } diff --git a/src/db/transactions/rollback.rs b/src/db/transactions/rollback.rs index 4546c88..f85b690 100644 --- a/src/db/transactions/rollback.rs +++ b/src/db/transactions/rollback.rs @@ -28,8 +28,8 @@ pub fn rollback_transaction_entry( .tables .remove(new_table_name.as_str()) .ok_or(format!("Table `{}` does not exist", new_table_name))?; - table.rollback_name(); - database.tables.insert(table.name()?.clone(), table); + table.last_mut().unwrap().rollback_name(); + database.tables.insert(table.last().unwrap().name()?.clone(), table); } }, SqlStatement::Select(_) => {} // These should be kept in the log but obv do nothing. From db2d68ee94235e64653761b90385c03c56249247 Mon Sep 17 00:00:00 2001 From: Fletcher555 Date: Thu, 18 Sep 2025 15:07:02 -0400 Subject: [PATCH 3/4] Add support for transactions with CREATE and DROP improving the table stack --- src/db/database.rs | 51 ++++++++++++++++++--- src/db/table/operations/alter_table/mod.rs | 18 +++----- src/db/table/operations/create_table/mod.rs | 2 +- src/db/table/test_utils.rs | 2 +- src/db/transactions/rollback.rs | 9 ++-- 5 files changed, 56 insertions(+), 26 deletions(-) diff --git a/src/db/database.rs b/src/db/database.rs index 82e7d50..f531155 100644 --- a/src/db/database.rs +++ b/src/db/database.rs @@ -8,7 +8,7 @@ use crate::interpreter::ast::SqlStatement; use std::collections::HashMap; pub struct Database { - pub tables: HashMap>, + pub tables: HashMap>>, pub transaction: TransactionLog, } @@ -113,21 +113,60 @@ impl Database { } pub fn has_table(&self, table_name: &str) -> bool { - self.tables.contains_key(table_name) + self.tables.contains_key(table_name) + && !self.tables.get(table_name).is_none() + && !self.tables.get(table_name).unwrap().is_empty() + && self.tables.get(table_name).unwrap().last().unwrap().is_some() } pub fn get_table(&self, table_name: &str) -> Result<&Table, String> { if !self.has_table(table_name) { return Err(format!("Table `{}` does not exist", table_name)); } - Ok(self.tables.get(table_name).unwrap().last().unwrap()) + let table = self.tables.get(table_name).unwrap().last().unwrap(); + match table { + Some(table) => Ok(table), + _ => Err(format!("Table `{}` does not exist", table_name)), + } } pub fn get_table_mut(&mut self, table_name: &str) -> Result<&mut Table, String> { if !self.has_table(table_name) { return Err(format!("Table `{}` does not exist", table_name)); } - Ok(self.tables.get_mut(table_name).unwrap().last_mut().unwrap()) + let table = self.tables.get_mut(table_name).unwrap().last_mut().unwrap(); + match table { + Some(table) => Ok(table), + _ => Err(format!("Table `{}` does not exist", table_name)), + } + } + + pub fn push_table_change(&mut self, table_name: &str, table: Table) { + if !self.has_table(table_name) { + self.tables.insert(table_name.to_string(), vec![Some(table)]); + } + else { + self.tables.get_mut(table_name).unwrap().push(Some(table)); + } + } + + pub fn pop_table_change(&mut self, table_name: &str) -> Result { + if !self.has_table(table_name) { + return Err(format!("Table `{}` does not exist", table_name)); + } + + let table = self.tables.get_mut(table_name).unwrap().pop().unwrap(); + + // Check if vector is empty before removing key + let is_empty = self.tables.get(table_name).unwrap().is_empty(); + if is_empty { + self.tables.remove(table_name); + } + + match table { + Some(table) => Ok(table), + _ => Err(format!("Table `{}` does not exist", table_name)), + } } } @@ -140,7 +179,7 @@ mod tests { Database { tables: HashMap::from([( "users".to_string(), - vec![Table::new( + vec![Some(Table::new( "users".to_string(), vec![ ColumnDefinition { @@ -154,7 +193,7 @@ mod tests { constraints: vec![], }, ], - )], + ))], )]), transaction: TransactionLog { entries: None }, } diff --git a/src/db/table/operations/alter_table/mod.rs b/src/db/table/operations/alter_table/mod.rs index 95dddfc..ddbd5a8 100644 --- a/src/db/table/operations/alter_table/mod.rs +++ b/src/db/table/operations/alter_table/mod.rs @@ -9,15 +9,9 @@ pub fn alter_table( ) -> Result<(), String> { return match statement.action { AlterTableAction::RenameTable { new_table_name } => { - let table = database.tables.remove(&statement.table_name); - match table { - Some(mut table_stack) => { - let table = table_stack.last_mut().unwrap(); - table.change_name(new_table_name, is_transaction); - database.tables.insert(table.name()?.clone(), table_stack); - } - None => return Err(format!("Table `{}` does not exist", statement.table_name)), - }; + let mut table = database.pop_table_change(statement.table_name.as_str())?; + table.change_name(new_table_name.clone(), is_transaction); + database.push_table_change(new_table_name.as_str(), table); Ok(()) } AlterTableAction::RenameColumn { @@ -114,9 +108,9 @@ mod tests { }; let result = alter_table(&mut database, statement, false); assert!(result.is_ok()); - assert!(!database.tables.contains_key("users")); - assert!(database.tables.contains_key("new_users")); - assert!(database.tables.get("new_users").unwrap().last().unwrap().name().unwrap() == "new_users"); + assert!(!database.has_table("users")); + assert!(database.has_table("new_users")); + assert!(database.get_table("new_users").unwrap().name().unwrap() == "new_users"); } #[test] diff --git a/src/db/table/operations/create_table/mod.rs b/src/db/table/operations/create_table/mod.rs index c999022..5e883fc 100644 --- a/src/db/table/operations/create_table/mod.rs +++ b/src/db/table/operations/create_table/mod.rs @@ -17,7 +17,7 @@ pub fn create_table( } } let table = Table::new(statement.table_name, statement.columns); - database.tables.insert(table.name()?.clone(), vec![table]); + database.tables.insert(table.name()?.clone(), vec![Some(table)]); Ok(()) } diff --git a/src/db/table/test_utils.rs b/src/db/table/test_utils.rs index 38038bb..9da7c50 100644 --- a/src/db/table/test_utils.rs +++ b/src/db/table/test_utils.rs @@ -70,7 +70,7 @@ pub fn default_table() -> Table { #[cfg(test)] pub fn default_database() -> Database { let mut database = Database::new(); - database.tables.insert("users".to_string(), vec![default_table()]); + database.tables.insert("users".to_string(), vec![Some(default_table())]); database } diff --git a/src/db/transactions/rollback.rs b/src/db/transactions/rollback.rs index f85b690..91d16b6 100644 --- a/src/db/transactions/rollback.rs +++ b/src/db/transactions/rollback.rs @@ -24,12 +24,9 @@ pub fn rollback_transaction_entry( } AlterTableAction::RenameTable { ref new_table_name } => { // It is now under the new name - let mut table = database - .tables - .remove(new_table_name.as_str()) - .ok_or(format!("Table `{}` does not exist", new_table_name))?; - table.last_mut().unwrap().rollback_name(); - database.tables.insert(table.last().unwrap().name()?.clone(), table); + let mut table = database.pop_table_change(new_table_name.as_str())?; + table.rollback_name(); + database.push_table_change(statement.table_name.as_str(), table); } }, SqlStatement::Select(_) => {} // These should be kept in the log but obv do nothing. From f2e044a9ecb9e72e506c50fae8fc08e6787409b8 Mon Sep 17 00:00:00 2001 From: Fletcher555 Date: Thu, 18 Sep 2025 15:11:52 -0400 Subject: [PATCH 4/4] Add support for DROP TABLE statements within transactions --- src/db/database.rs | 24 ++++++++++------ src/db/table/operations/create_table/mod.rs | 4 ++- src/db/table/operations/drop_table/mod.rs | 22 ++++++++++---- src/db/table/test_utils.rs | 4 ++- src/db/transactions/rollback.rs | 9 +++++- tests/transaction_test.rs | 32 +++++++++++++++++++++ 6 files changed, 78 insertions(+), 17 deletions(-) diff --git a/src/db/database.rs b/src/db/database.rs index f531155..e493dd9 100644 --- a/src/db/database.rs +++ b/src/db/database.rs @@ -54,7 +54,7 @@ impl Database { Ok(None) } SqlStatement::DropTable(statement) => { - drop_table::drop_table(self, statement)?; + drop_table::drop_table(self, statement, self.transaction.in_transaction())?; self.transaction.append_entry(sql_statement_clone, vec![])?; Ok(None) } @@ -113,10 +113,16 @@ impl Database { } pub fn has_table(&self, table_name: &str) -> bool { - self.tables.contains_key(table_name) + self.tables.contains_key(table_name) && !self.tables.get(table_name).is_none() && !self.tables.get(table_name).unwrap().is_empty() - && self.tables.get(table_name).unwrap().last().unwrap().is_some() + && self + .tables + .get(table_name) + .unwrap() + .last() + .unwrap() + .is_some() } pub fn get_table(&self, table_name: &str) -> Result<&Table, String> { @@ -143,9 +149,9 @@ impl Database { pub fn push_table_change(&mut self, table_name: &str, table: Table) { if !self.has_table(table_name) { - self.tables.insert(table_name.to_string(), vec![Some(table)]); - } - else { + self.tables + .insert(table_name.to_string(), vec![Some(table)]); + } else { self.tables.get_mut(table_name).unwrap().push(Some(table)); } } @@ -154,15 +160,15 @@ impl Database { if !self.has_table(table_name) { return Err(format!("Table `{}` does not exist", table_name)); } - + let table = self.tables.get_mut(table_name).unwrap().pop().unwrap(); - + // Check if vector is empty before removing key let is_empty = self.tables.get(table_name).unwrap().is_empty(); if is_empty { self.tables.remove(table_name); } - + match table { Some(table) => Ok(table), _ => Err(format!("Table `{}` does not exist", table_name)), diff --git a/src/db/table/operations/create_table/mod.rs b/src/db/table/operations/create_table/mod.rs index 5e883fc..0fe8542 100644 --- a/src/db/table/operations/create_table/mod.rs +++ b/src/db/table/operations/create_table/mod.rs @@ -17,7 +17,9 @@ pub fn create_table( } } let table = Table::new(statement.table_name, statement.columns); - database.tables.insert(table.name()?.clone(), vec![Some(table)]); + database + .tables + .insert(table.name()?.clone(), vec![Some(table)]); Ok(()) } diff --git a/src/db/table/operations/drop_table/mod.rs b/src/db/table/operations/drop_table/mod.rs index 38190c4..1217fa1 100644 --- a/src/db/table/operations/drop_table/mod.rs +++ b/src/db/table/operations/drop_table/mod.rs @@ -1,7 +1,11 @@ use crate::db::database::Database; use crate::interpreter::ast::{DropTableStatement, ExistenceCheck}; -pub fn drop_table(database: &mut Database, statement: DropTableStatement) -> Result<(), String> { +pub fn drop_table( + database: &mut Database, + statement: DropTableStatement, + is_transaction: bool, +) -> Result<(), String> { if !database.has_table(&statement.table_name) { match statement.existence_check { Some(ExistenceCheck::IfExists) => { @@ -12,7 +16,15 @@ pub fn drop_table(database: &mut Database, statement: DropTableStatement) -> Res } } } - database.tables.remove(&statement.table_name); + if is_transaction { + database + .tables + .get_mut(&statement.table_name) + .unwrap() + .push(None); + } else { + database.tables.remove(&statement.table_name); + } Ok(()) } @@ -28,7 +40,7 @@ mod tests { existence_check: None, }; let mut database = default_database(); - let result = drop_table(&mut database, statement); + let result = drop_table(&mut database, statement, false); assert!(result.is_ok()); assert!(!database.has_table("users")); } @@ -40,7 +52,7 @@ mod tests { existence_check: None, }; let mut database = Database::new(); - let result = drop_table(&mut database, statement); + let result = drop_table(&mut database, statement, false); assert!(result.is_err()); assert_eq!("Table `users` does not exist", result.err().unwrap()); } @@ -52,7 +64,7 @@ mod tests { existence_check: Some(ExistenceCheck::IfExists), }; let mut database = Database::new(); - let result = drop_table(&mut database, statement); + let result = drop_table(&mut database, statement, false); assert!(result.is_ok()); } } diff --git a/src/db/table/test_utils.rs b/src/db/table/test_utils.rs index 9da7c50..d093b0c 100644 --- a/src/db/table/test_utils.rs +++ b/src/db/table/test_utils.rs @@ -70,7 +70,9 @@ pub fn default_table() -> Table { #[cfg(test)] pub fn default_database() -> Database { let mut database = Database::new(); - database.tables.insert("users".to_string(), vec![Some(default_table())]); + database + .tables + .insert("users".to_string(), vec![Some(default_table())]); database } diff --git a/src/db/transactions/rollback.rs b/src/db/transactions/rollback.rs index 91d16b6..93876f2 100644 --- a/src/db/transactions/rollback.rs +++ b/src/db/transactions/rollback.rs @@ -32,7 +32,14 @@ pub fn rollback_transaction_entry( SqlStatement::Select(_) => {} // These should be kept in the log but obv do nothing. SqlStatement::CreateTable(_) => { database.tables.remove(statement.table_name.as_str()); - }, + } + SqlStatement::DropTable(statement) => { + database + .tables + .get_mut(statement.table_name.as_str()) + .unwrap() + .pop(); + } _ => return Err("UNSUPPORTED".to_string()), } return Ok(()); diff --git a/tests/transaction_test.rs b/tests/transaction_test.rs index 9c76ab3..bf042cb 100644 --- a/tests/transaction_test.rs +++ b/tests/transaction_test.rs @@ -87,3 +87,35 @@ fn test_transaction_create_table() { assert_eq!(expected[i], *result); } } + +#[test] +fn test_transaction_drop_table() { + let mut database = Database::new(); + let sql = " + CREATE TABLE users ( + id INTEGER, + name TEXT + ); + INSERT INTO users (id, name) VALUES (1, 'John'); + BEGIN; + SELECT * FROM users; + DROP TABLE users; + SELECT * FROM users; + ROLLBACK; + SELECT * FROM users; + "; + let result = run_sql(&mut database, sql); + let expected = vec![ + Ok(None), + Ok(None), + Ok(None), + Ok(Some(vec![Row(vec![Value::Integer(1), Value::Text("John".to_string())])])), + Ok(None), + Err("Execution Error with statement starting on line 10 \n Error: Table `users` does not exist".to_string()), + Ok(None), + Ok(Some(vec![Row(vec![Value::Integer(1), Value::Text("John".to_string())])])), + ]; + for (i, result) in result.iter().enumerate() { + assert_eq!(expected[i], *result); + } +}