From dc3e9b0077bf4edef3e72347d8834f3eafdf0280 Mon Sep 17 00:00:00 2001 From: Yuriy Dupyn <2153100+omedusyo@users.noreply.github.com> Date: Thu, 28 Dec 2023 12:33:00 +0100 Subject: [PATCH] Add some basic tests --- minisql/src/main.rs | 295 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 248 insertions(+), 47 deletions(-) diff --git a/minisql/src/main.rs b/minisql/src/main.rs index 999f49a..2008db9 100644 --- a/minisql/src/main.rs +++ b/minisql/src/main.rs @@ -5,6 +5,7 @@ use bimap::BiMap; // TODO: Note that every operation has a table name. // Perhaps consider factoring the table name out // and think of the operations as operating on a unique table. +// TODO: `TableName` should be replaced by `TablePosition` enum Operation { Select(TableName, ColumnSelection, Option), Insert(TableName, InsertionValues), @@ -92,6 +93,7 @@ impl DbValue { type TableName = String; type TablePosition = usize; +#[derive(Debug)] struct Table { schema: TableSchema, rows: Rows, // TODO: Consider wrapping this in a lock. Also consider if we need to have the @@ -104,12 +106,14 @@ struct Table { // Maybe we should have a separate index type for each type of value we're indexing over // TODO: I should have a set of UUID, not just a single UUID, e.g. // a user table can have multiple different users with the same name. +#[derive(Debug)] struct ColumnIndex { index: BTreeMap> } // Note that it is nice to split metadata from the data because // then you can give the metadata to the parser without giving it the data. +#[derive(Debug)] struct TableSchema { table_name: TableName, // used for descriptive errors primary_key: ColumnPosition, @@ -150,12 +154,20 @@ fn select_columns(row: &Row, columns: &Vec) -> Row { } // ==============Interpreter================ +#[derive(Debug)] struct State { table_name_position_mapping: BiMap, tables: Vec, } impl State { + fn new() -> Self { + Self { + table_name_position_mapping: BiMap::new(), + tables: vec![], + } + } + fn table_from_name<'b: 'a, 'a>(&'b self, table_name: &TableName) -> DbResult<&'a Table> { match self.table_name_position_mapping.get_by_left(table_name) { Some(table_position) => { @@ -181,6 +193,54 @@ impl State { self.table_name_position_mapping.insert(table_name, new_table_position); self.tables.push(table); } + + // TODO: Decide if we want for this to return a response (but then you have to deal with lifetimes, + // because you'll be forced to put an iterator/slice into the Response data-structure. + // Alternative is to pass a row-consumer to the functionas that knows how to communicate with + // the client, but the details of communication are hidden behind an interface + // + // writer: impl SqlConsumer + fn interpret(&mut self, operation: Operation) -> DbResult { + // TODO: lock stuff + use Operation::*; + + match operation { + Select(table_name, column_selection, maybe_condition) => { + let table: &Table = self.table_from_name(&table_name)?; + Ok(Response::Selected(table.select_where(column_selection, maybe_condition)?)) + }, + Insert(table_name, values) => { + let table: &mut Table = self.table_from_name_mut(&table_name)?; + + let _ = table.insert(values)?; + Ok(Response::Inserted) + }, + Delete(table_name, maybe_condition) => { + let table: &mut Table = self.table_from_name_mut(&table_name)?; + + let rows_affected = table.delete_where(maybe_condition)?; + Ok(Response::Deleted(rows_affected)) + }, + CreateTable(table_name, table_schema) => { + let table = Table::new(table_schema); + self.attach_table(table_name, table); + Ok(Response::TableCreated) + }, + CreateIndex(table_name, column_name) => { + // TODO: This is incomplete. It can happen that an index is created + // after the table has some rows for a while. + // In such a case the index needs to be built over all those existing rows. + let table: &mut Table = self.table_from_name_mut(&table_name)?; + let column_position: ColumnPosition = table.schema.column_position_from_column_name(&column_name)?; + + let mut index: ColumnIndex = ColumnIndex::new(); + let _ = index.update_from_table(&table, column_position)?; + + table.attach_index(column_position, index); + Ok(Response::IndexCreated) + }, + } + } } // TODO: Give a better name to something that you can respond to with rows @@ -188,51 +248,6 @@ trait SqlConsumer { // TODO: } -// TODO: Decide if we want for this to return a response (but then you have to deal with lifetimes, -// because you'll be forced to put an iterator/slice into the Response data-structure. -// Alternative is to pass a row-consumer to the functionas that knows how to communicate with -// the client, but the details of communication are hidden behind an interface -fn interpret(table_name: TableName, operation: Operation, state: &mut State, consumer: impl SqlConsumer) -> DbResult { - // TODO: lock stuff - use Operation::*; - - match operation { - Select(table_name, column_selection, maybe_condition) => { - let table: &Table = state.table_from_name(&table_name)?; - Ok(Response::Selected(table.select_where(column_selection, maybe_condition)?)) - }, - Insert(table_name, values) => { - let table: &mut Table = state.table_from_name_mut(&table_name)?; - - let _ = table.insert(values)?; - Ok(Response::Inserted) - }, - Delete(table_name, maybe_condition) => { - let table: &mut Table = state.table_from_name_mut(&table_name)?; - - let rows_affected = table.delete_where(maybe_condition)?; - Ok(Response::Deleted(rows_affected)) - }, - CreateTable(table_name, table_schema) => { - let table = Table::new(table_schema); - state.attach_table(table_name, table); - Ok(Response::TableCreated) - }, - CreateIndex(table_name, column_name) => { - // TODO: This is incomplete. It can happen that an index is created - // after the table has some rows for a while. - // In such a case the index needs to be built over all those existing rows. - let table: &mut Table = state.table_from_name_mut(&table_name)?; - let column_position: ColumnPosition = table.schema.column_position_from_column_name(&column_name)?; - - let mut index: ColumnIndex = ColumnIndex::new(); - let _ = index.update_from_table(&table, column_position)?; - - table.attach_index(column_position, index); - Ok(Response::IndexCreated) - }, - } -} impl TableSchema { fn get_column(&self, column_name: &ColumnName) -> DbResult<(DbType, ColumnPosition)> { @@ -304,7 +319,7 @@ impl TableSchema { return Err(Error::MismatchBetweenInsertValuesAndColumns(self.table_name.clone(), insertion_values)) } - let mut row: Vec = Vec::with_capacity(number_of_columns); + let mut row: Row = Vec::with_capacity(number_of_columns); let mut values: HashMap = HashMap::new(); for (column_name, db_value) in &insertion_values { @@ -564,6 +579,7 @@ impl ColumnIndex { } } +#[derive(Debug)] enum Response { Selected(Vec), Inserted, @@ -574,7 +590,7 @@ enum Response { type DbResult = Result; -// #[derive(Debug)] +#[derive(Debug)] enum Error { TableDoesNotExist(TableName), ColumnDoesNotExist(TableName, ColumnName), @@ -590,3 +606,188 @@ enum Error { fn main() { println!("Hello, world!"); } + + + +#[cfg(test)] +mod tests { + use super::*; + + fn users_schema() -> TableSchema { + let id: ColumnPosition = 0; + let name: ColumnPosition = 1; + let age: ColumnPosition = 2; + + TableSchema { + table_name: "users".to_string(), + primary_key: 0, + column_name_position_mapping: { + let mut mapping: BiMap = BiMap::new(); + mapping.insert("id".to_string(), id); + mapping.insert("name".to_string(), name); + mapping.insert("age".to_string(), age); + mapping + }, + types: vec![DbType::UUID, DbType::String, DbType::Int], + } + } + + #[test] + fn test_table_creation() { + let mut state = State::new(); + let users_schema = users_schema(); + let users = users_schema.table_name.clone(); + + state.interpret(Operation::CreateTable(users.clone(), users_schema)).unwrap(); + + assert!(state.tables.len() == 1); + let table = &state.tables[0]; + assert!(table.rows.len() == 0); + + assert!(table.schema.table_name == users); + } + + #[test] + fn test_select_empty() { + let mut state = State::new(); + let users_schema = users_schema(); + let users = users_schema.table_name.clone(); + + state.interpret(Operation::CreateTable(users.clone(), users_schema)).unwrap(); + let response: Response = state.interpret(Operation::Select(users.clone(), ColumnSelection::All, None)).unwrap(); + assert!(matches!(response, Response::Selected(_))); + let Response::Selected(rows) = response else { todo!() }; + assert!(rows.len() == 0); + + } + + #[test] + fn test_select_nonexistant_table() { + let mut state = State::new(); + + let response: DbResult = state.interpret(Operation::Select("table_that_doesnt_exist".to_string(), ColumnSelection::All, None)); + assert!(matches!(response, Err(Error::TableDoesNotExist(_)))); + } + + #[test] + fn test_insert_select_basic1() { + use DbValue::*; + use IndexableDbValue::*; + + let mut state = State::new(); + let users_schema = users_schema(); + let users = users_schema.table_name.clone(); + + state.interpret(Operation::CreateTable(users.clone(), users_schema)).unwrap(); + + let (id, name, age) = ( + Indexable(UUID(0)), + Indexable(String("Plato".to_string())), + Indexable(Int(64)) + ); + state.interpret(Operation::Insert(users.clone(), vec![ + ("id".to_string(), id.clone()), + ("name".to_string(), name.clone()), + ("age".to_string(), age.clone()), + ])).unwrap(); + + let response: Response = state.interpret(Operation::Select(users.clone(), ColumnSelection::All, None)).unwrap(); + + assert!(matches!(response, Response::Selected(_))); + let Response::Selected(rows) = response else { todo!() }; + assert!(rows.len() == 1); + let row = &rows[0]; + + assert!(row.len() == 3); + assert!(row[0] == id); + assert!(row[1] == name); + assert!(row[2] == age); + } + + #[test] + fn test_insert_select_basic2() { + use DbValue::*; + use IndexableDbValue::*; + use Operation::*; + use ColumnSelection::*; + use Condition::*; + + let mut state = State::new(); + let users_schema = users_schema(); + let users = users_schema.table_name.clone(); + + state.interpret(CreateTable(users.clone(), users_schema)).unwrap(); + + let response0: Response = state.interpret(Select(users.clone(), ColumnSelection::All, None)).unwrap(); + assert!(matches!(response0, Response::Selected(_))); + let Response::Selected(rows0) = response0 else { todo!() }; + assert!(rows0.len() == 0); + + let (id0, name0, age0) = ( + Indexable(UUID(0)), + Indexable(String("Plato".to_string())), + Indexable(Int(64)) + ); + state.interpret(Insert(users.clone(), vec![ + ("id".to_string(), id0.clone()), + ("name".to_string(), name0.clone()), + ("age".to_string(), age0.clone()), + ])).unwrap(); + + let (id1, name1, age1) = ( + Indexable(UUID(1)), + Indexable(String("Aristotle".to_string())), + Indexable(Int(20)) + ); + state.interpret(Insert(users.clone(), vec![ + ("id".to_string(), id1.clone()), + ("name".to_string(), name1.clone()), + ("age".to_string(), age1.clone()), + ])).unwrap(); + + { + let response: Response = state.interpret(Select(users.clone(), All, None)).unwrap(); + + assert!(matches!(response, Response::Selected(_))); + let Response::Selected(rows) = response else { todo!() }; + assert!(rows.len() == 2); + let row0 = &rows[0]; + let row1 = &rows[1]; + + assert!(row0.len() == 3); + assert!(row0[0] == id0); + assert!(row0[1] == name0); + assert!(row0[2] == age0); + + assert!(row1.len() == 3); + assert!(row1[0] == id1); + assert!(row1[1] == name1); + assert!(row1[2] == age1); + } + + { + let response: Response = state.interpret(Select(users.clone(), All, Some(Eq("id".to_string(), id0.clone())))).unwrap(); + assert!(matches!(response, Response::Selected(_))); + let Response::Selected(rows) = response else { todo!() }; + assert!(rows.len() == 1); + let row0 = &rows[0]; + + assert!(row0.len() == 3); + assert!(row0[0] == id0); + assert!(row0[1] == name0); + assert!(row0[2] == age0); + } + + { + let response: Response = state.interpret(Select(users.clone(), Columns(vec!["name".to_string(), "id".to_string()]), Some(Eq("id".to_string(), id0.clone())))).unwrap(); + assert!(matches!(response, Response::Selected(_))); + let Response::Selected(rows) = response else { todo!() }; + assert!(rows.len() == 1); + let row0 = &rows[0]; + + assert!(row0.len() == 2); + assert!(row0[0] == name0); + assert!(row0[1] == id0); + } + } +}