diff --git a/minisql/src/base.rs b/minisql/src/base.rs new file mode 100644 index 0000000..eeb74a1 --- /dev/null +++ b/minisql/src/base.rs @@ -0,0 +1,132 @@ +use std::collections::HashMap; +use bimap::BiMap; +use crate::operation::{InsertionValues, ColumnSelection}; +use crate::table::Row; +use crate::type_system::{DbType, DbValue, IndexableDbValue, UUID}; +use crate::error::Error; + +// 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)] +pub struct TableSchema { + pub table_name: TableName, // used for descriptive errors + pub primary_key: ColumnPosition, + pub column_name_position_mapping: BiMap, + pub types: Vec, +} + +pub type TableName = String; + +pub type ColumnName = String; +pub type ColumnPosition = usize; + +pub type DbResult = Result; + +impl TableSchema { + fn get_column(&self, column_name: &ColumnName) -> DbResult<(DbType, ColumnPosition)> { + match self.column_name_position_mapping.get_by_left(column_name) { + Some(column_position) => { + match self.types.get(*column_position) { + Some(type_) => { + Ok((*type_, *column_position)) + }, + None => { + Err(Error::MissingTypeAnnotationOfColumn(self.table_name.clone(), *column_position)) + } + } + }, + None => Err(Error::ColumnDoesNotExist(self.table_name.clone(), column_name.clone())) + } + } + + pub fn column_position_from_column_name(&self, column_name: &ColumnName) -> DbResult { + self.get_column(column_name).map(|(_, column_position)| column_position) + } + + pub fn is_primary(&self, column_position: ColumnPosition) -> bool { + self.primary_key == column_position + } + + fn column_positions_from_column_names(&self, column_names: &[ColumnName]) -> DbResult> { + let mut positions: Vec = Vec::with_capacity(column_names.len()); + for column_name in column_names { + let column_position = self.column_position_from_column_name(column_name)?; + positions.push(column_position) + } + Ok(positions) + } + + pub fn column_name_from_column_position(&self, column_position: ColumnPosition) -> DbResult { + match self.column_name_position_mapping.get_by_right(&column_position) { + Some(column_name) => Ok(column_name.clone()), + None => Err(Error::ColumnPositionDoesNotExist(self.table_name.clone(), column_position)) + } + } + + pub fn column_positions_from_column_selection(&self, column_selection: &ColumnSelection) -> DbResult> { + match column_selection { + ColumnSelection::All => { + let mut column_positions: Vec = self.column_name_position_mapping.iter().map(|(_, column_position)| *column_position).collect(); + column_positions.sort(); + Ok(column_positions) + }, + + ColumnSelection::Columns(column_names) => { + self.column_positions_from_column_names(column_names) + }, + } + } + + fn number_of_columns(&self) -> usize { + self.column_name_position_mapping.len() + } + + // TODO: IS THIS THE RIGHT PLACE? + pub fn row_from_insertion_values(&self, insertion_values: InsertionValues) -> DbResult<(UUID, Row)> { + // TODO: There should be proper validation of the insertion_values. + // And it shouldn't really be done here. + // + // In the below we don't check for duplicate column names + // + let number_of_columns = self.number_of_columns(); + if number_of_columns != insertion_values.len() { + return Err(Error::MismatchBetweenInsertValuesAndColumns(self.table_name.clone(), insertion_values)) + } + + let mut row: Row = Vec::with_capacity(number_of_columns); + + let mut values: HashMap = HashMap::new(); + for (column_name, db_value) in &insertion_values { + values.insert(column_name.clone(), db_value.clone()); + } + + for column_position in 0..number_of_columns { + let column_name: ColumnName = self.column_name_from_column_position(column_position)?; + match values.get(&column_name) { + Some(db_value) => { + row.push(db_value.clone()) + }, + None => { + return Err(Error::MissingColumnInInsertValues(self.table_name.clone(), column_name, insertion_values)) + } + } + } + + let id = match row.get(self.primary_key) { + Some(val) => { + match val { + DbValue::Indexable(IndexableDbValue::UUID(id)) => { + id + }, + _ => + unreachable!() + } + }, + None => + unreachable!() + }; + + Ok((*id, row)) + } + +} diff --git a/minisql/src/column_index.rs b/minisql/src/column_index.rs new file mode 100644 index 0000000..7be8671 --- /dev/null +++ b/minisql/src/column_index.rs @@ -0,0 +1,71 @@ +use std::collections::{BTreeMap, HashSet}; +use crate::base::{ColumnPosition, ColumnName, DbResult}; +use crate::type_system::{UUID, DbValue, IndexableDbValue}; +use crate::table::Table; +use crate::error::Error; + +#[derive(Debug)] +pub struct ColumnIndex { + index: BTreeMap> +} + +impl ColumnIndex { + pub fn new() -> Self { + let index = BTreeMap::new(); + Self { index } + } + + pub fn get(&self, value: &IndexableDbValue) -> HashSet { + match self.index.get(value) { + Some(set) => set.clone(), + None => HashSet::new(), + } + } + + pub fn add(&mut self, value: IndexableDbValue, id: UUID) { + match self.index.get_mut(&value) { + Some(ids) => { + ids.insert(id); + }, + None => { + self.index.insert(value, HashSet::from([id])); + } + } + } + + + // TODO: IS THIS THE RIGHT PLACE? + // Should be used in the case when an indexed is created after the table has existed for a + // while. In such a case you need to build the index from the already existing rows. + pub fn update_from_table(&mut self, table: &Table, column_position: ColumnPosition) -> DbResult<()> { + for (id, row) in &table.rows { + let value = match row.get(column_position) { + Some(DbValue::Indexable(value)) => { + value.clone() + }, + Some(_) => { + let column_name: ColumnName = table.schema.column_name_from_column_position(column_position)?; + return Err(Error::AttemptToIndexNonIndexableColumn(table.schema.table_name.to_string(), column_name)) + }, + None => { + return Err(Error::ColumnPositionDoesNotExist(table.schema.table_name.to_string(), column_position)) + } + }; + self.add(value, *id) + } + Ok(()) + } + + pub fn remove(&mut self, value: &IndexableDbValue, id_to_be_removed: UUID) -> bool { + match self.index.get_mut(value) { + Some(ids) => { + let was_present = ids.remove(&id_to_be_removed); + was_present + }, + None => { + false + } + } + } +} + diff --git a/minisql/src/error.rs b/minisql/src/error.rs new file mode 100644 index 0000000..126a855 --- /dev/null +++ b/minisql/src/error.rs @@ -0,0 +1,16 @@ +use crate::base::{ColumnName, TableName, ColumnPosition}; +use crate::type_system::{DbType, DbValue, UUID}; +use crate::operation::InsertionValues; + +#[derive(Debug)] +pub enum Error { + TableDoesNotExist(TableName), + ColumnDoesNotExist(TableName, ColumnName), + ColumnPositionDoesNotExist(TableName, ColumnPosition), + ValueDoesNotMatchExpectedType(TableName, ColumnName, DbType, DbValue), + AttemptingToInsertAlreadyPresentId(TableName, UUID), + MissingTypeAnnotationOfColumn(TableName, ColumnPosition), + MissingColumnInInsertValues(TableName, ColumnName, InsertionValues), + MismatchBetweenInsertValuesAndColumns(TableName, InsertionValues), + AttemptToIndexNonIndexableColumn(TableName, ColumnName), +} diff --git a/minisql/src/interpreter.rs b/minisql/src/interpreter.rs new file mode 100644 index 0000000..ee77ece --- /dev/null +++ b/minisql/src/interpreter.rs @@ -0,0 +1,430 @@ +use bimap::BiMap; +use crate::type_system::{DbValue, DbType, IndexableDbValue}; +use crate::base::{TableName, TableSchema, ColumnPosition, ColumnName, DbResult}; +use crate::table::{Table, Row}; +use crate::error::Error; +use crate::operation::{Operation, Condition, ColumnSelection}; +use crate::column_index::ColumnIndex; + + +// Use `TablePosition` as index +pub type Tables = Vec; +pub type TablePosition = usize; + +// ==============Interpreter================ +#[derive(Debug)] +pub struct State { + table_name_position_mapping: BiMap, + tables: Tables, +} + +#[derive(Debug)] +pub enum Response { + Selected(Vec), + Inserted, + Deleted(usize), // how many were deleted + TableCreated, + IndexCreated, +} + + +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) => { + let table = &self.tables[*table_position]; + Ok(table) + }, + None => Err(Error::TableDoesNotExist(table_name.clone())) + } + } + + fn table_from_name_mut<'b: 'a, 'a>(&'b mut self, table_name: &TableName) -> DbResult<&'a mut Table> { + match self.table_name_position_mapping.get_by_left(table_name) { + Some(table_position) => { + let table = &mut self.tables[*table_position]; + Ok(table) + }, + None => Err(Error::TableDoesNotExist(table_name.clone())) + } + } + + fn attach_table(&mut self, table_name: TableName, table: Table) { + let new_table_position: TablePosition = self.tables.len(); + 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 SqlResponseConsumer + 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) => { + 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 +trait SqlResponseConsumer { + // TODO: +} + + +#[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: id, + 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 (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); + } + } + + #[test] + fn test_delete() { + 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 (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 delete_response: Response = state.interpret(Delete(users.clone(), Some(Eq("id".to_string(), id0.clone())))).unwrap(); + assert!(matches!(delete_response, Response::Deleted(1))); + + 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() == 1); + let row = &rows[0]; + + assert!(row.len() == 3); + assert!(row[0] == id1); + assert!(row[1] == name1); + assert!(row[2] == age1); + } + + // TODO: Test CreateIndex +} + + +pub fn example() { + use DbValue::*; + use IndexableDbValue::*; + use Operation::*; + use ColumnSelection::*; + use Condition::*; + + let users_schema = { + let id: ColumnPosition = 0; + let name: ColumnPosition = 1; + let age: ColumnPosition = 2; + + TableSchema { + table_name: "users".to_string(), + primary_key: id, + 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], + } + }; + let users = users_schema.table_name.clone(); + + + let mut state = State::new(); + state.interpret(Operation::CreateTable(users.clone(), users_schema)).unwrap(); + + let (id0, name0, age0) = ( + Indexable(UUID(0)), + Indexable(String("Plato".to_string())), + Indexable(Int(64)) + ); + println!("==INSERT Plato=="); + 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)) + ); + println!("==INSERT Aristotle=="); + state.interpret(Insert(users.clone(), vec![ + ("id".to_string(), id1.clone()), + ("name".to_string(), name1.clone()), + ("age".to_string(), age1.clone()), + ])).unwrap(); + println!(); + + { + let response: Response = state.interpret(Operation::Select(users.clone(), ColumnSelection::All, None)).unwrap(); + println!("==SELECT ALL=="); + println!("{:?}", response); + println!(); + } + { + let response: Response = state.interpret(Select(users.clone(), All, Some(Eq("id".to_string(), id0.clone())))).unwrap(); + println!("==SELECT Plato=="); + println!("{:?}", response); + println!(); + } + + { + let _delete_response: Response = state.interpret(Delete(users.clone(), Some(Eq("id".to_string(), id0.clone())))).unwrap(); + println!("==DELETE Plato=="); + let response: Response = state.interpret(Select(users.clone(), Columns(vec!["name".to_string(), "id".to_string()]), None)).unwrap(); + println!("==SELECT All=="); + println!("{:?}", response); + println!(); + } + +} diff --git a/minisql/src/main.rs b/minisql/src/main.rs index 3e5b530..0a9fd94 100644 --- a/minisql/src/main.rs +++ b/minisql/src/main.rs @@ -1,903 +1,11 @@ -use std::collections::{BTreeMap, HashMap, HashSet}; -use bimap::BiMap; - -// ==============SQL operations================ -// 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), - Delete(TableName, Option), - // Update(...), - CreateTable(TableName, TableSchema), - CreateIndex(TableName, ColumnName), - // DropTable(TableName), -} - -type InsertionValues = Vec<(ColumnName, DbValue)>; - -enum ColumnSelection { - All, - Columns(Vec), -} - -enum Condition { - // And(Box, Box), - // Or(Box, Box), - // Not(Box), - - Eq(ColumnName, DbValue), - // LessOrEqual(ColumnName, DbValue), - // Less(ColumnName, DbValue), - - // StringCondition(StringCondition), -} - -// enum StringCondition { -// Prefix(ColumnName, String), -// Substring(ColumnName, String), -// } - - -// ==============Values and Types================ -type UUID = u64; - -// TODO: What about nulls? I would rather not have that in SQL, it sucks. -// I would rather have non-nullable values by default, -// and something like an explicit Option type for nulls. -#[derive(Debug, Clone, PartialEq)] -enum DbValue { - Number(f64), // TODO: Can't put floats as keys in maps, since they don't implement Eq. What to - // do? - Indexable(IndexableDbValue), -} - -#[derive(Debug, Ord, Eq, Clone, PartialOrd, PartialEq)] -enum IndexableDbValue { - String(String), - Int(u64), - UUID(UUID), - // TODO: what about null? -} - -#[derive(Debug, Clone, Copy)] -enum DbType { - String, - Int, - Number, - UUID, -} - -impl DbValue { - fn to_type(self) -> DbType { - match self { - Self::Number(_) => DbType::Number, - Self::Indexable(val) => - match val { - IndexableDbValue::String(_) => DbType::String, - IndexableDbValue::Int(_) => DbType::Int, - IndexableDbValue::UUID(_) => DbType::UUID, - } - } - } -} - - -// ==============Tables================ -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 - // same lock for both rows and indexes - indexes: - HashMap // TODO: Consider generalizing `ColumnPosition` to something that would also apply to a pair of `ColumnNames` etc -} - -#[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, - column_name_position_mapping: BiMap, - types: Vec, -} - -// Use `TablePosition` as index -type Tables = Vec
; - - -type ColumnName = String; -type ColumnPosition = usize; - -// Use `ColumnPosition` as index -type Row = Vec; - -type Rows = - // TODO: This should be some sort of an interface to a dictionary - // s.t. in the background it may modify stuff in memory or talk to the disk - BTreeMap; - - // interface - // insert(id, value) - -fn restrict_columns(row: &Row, columns: &Vec) -> Row { - // If the index from `columns` is non-existant in `row`, it will just ignore it. - let mut subrow: Row = vec![]; - for column_position in columns { - match row.get(*column_position) { - Some(value) => { - subrow.push(value.clone()) - }, - None => {} - } - } - subrow -} - -// ==============Interpreter================ -#[derive(Debug)] -struct State { - table_name_position_mapping: BiMap, - tables: Tables, -} - -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) => { - let table = &self.tables[*table_position]; - Ok(table) - }, - None => Err(Error::TableDoesNotExist(table_name.clone())) - } - } - - fn table_from_name_mut<'b: 'a, 'a>(&'b mut self, table_name: &TableName) -> DbResult<&'a mut Table> { - match self.table_name_position_mapping.get_by_left(table_name) { - Some(table_position) => { - let table = &mut self.tables[*table_position]; - Ok(table) - }, - None => Err(Error::TableDoesNotExist(table_name.clone())) - } - } - - fn attach_table(&mut self, table_name: TableName, table: Table) { - let new_table_position: TablePosition = self.tables.len(); - 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 SqlResponseConsumer - 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) => { - 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 -trait SqlResponseConsumer { - // TODO: -} - - -impl TableSchema { - fn get_column(&self, column_name: &ColumnName) -> DbResult<(DbType, ColumnPosition)> { - match self.column_name_position_mapping.get_by_left(column_name) { - Some(column_position) => { - match self.types.get(*column_position) { - Some(type_) => { - Ok((*type_, *column_position)) - }, - None => { - Err(Error::MissingTypeAnnotationOfColumn(self.table_name.clone(), *column_position)) - } - } - }, - None => Err(Error::ColumnDoesNotExist(self.table_name.clone(), column_name.clone())) - } - } - - fn column_position_from_column_name(&self, column_name: &ColumnName) -> DbResult { - self.get_column(column_name).map(|(_, column_position)| column_position) - } - - fn is_primary(&self, column_position: ColumnPosition) -> bool { - self.primary_key == column_position - } - - fn column_positions_from_column_names(&self, column_names: &[ColumnName]) -> DbResult> { - let mut positions: Vec = Vec::with_capacity(column_names.len()); - for column_name in column_names { - let column_position = self.column_position_from_column_name(column_name)?; - positions.push(column_position) - } - Ok(positions) - } - - fn column_name_from_column_position(&self, column_position: ColumnPosition) -> DbResult { - match self.column_name_position_mapping.get_by_right(&column_position) { - Some(column_name) => Ok(column_name.clone()), - None => Err(Error::ColumnPositionDoesNotExist(self.table_name.clone(), column_position)) - } - } - - fn column_positions_from_column_selection(&self, column_selection: &ColumnSelection) -> DbResult> { - match column_selection { - ColumnSelection::All => { - let mut column_positions: Vec = self.column_name_position_mapping.iter().map(|(_, column_position)| *column_position).collect(); - column_positions.sort(); - Ok(column_positions) - }, - - ColumnSelection::Columns(column_names) => { - self.column_positions_from_column_names(column_names) - }, - } - } - - fn number_of_columns(&self) -> usize { - self.column_name_position_mapping.len() - } - - fn row_from_insertion_values(&self, insertion_values: InsertionValues) -> DbResult<(UUID, Row)> { - // TODO: There should be proper validation of the insertion_values. - // And it shouldn't really be done here. - // - // In the below we don't check for duplicate column names - // - let number_of_columns = self.number_of_columns(); - if number_of_columns != insertion_values.len() { - return Err(Error::MismatchBetweenInsertValuesAndColumns(self.table_name.clone(), insertion_values)) - } - - let mut row: Row = Vec::with_capacity(number_of_columns); - - let mut values: HashMap = HashMap::new(); - for (column_name, db_value) in &insertion_values { - values.insert(column_name.clone(), db_value.clone()); - } - - for column_position in 0..number_of_columns { - let column_name: ColumnName = self.column_name_from_column_position(column_position)?; - match values.get(&column_name) { - Some(db_value) => { - row.push(db_value.clone()) - }, - None => { - return Err(Error::MissingColumnInInsertValues(self.table_name.clone(), column_name, insertion_values)) - } - } - } - - let id = match row.get(self.primary_key) { - Some(val) => { - match val { - DbValue::Indexable(IndexableDbValue::UUID(id)) => { - id - }, - _ => - unreachable!() - } - }, - None => - unreachable!() - }; - - Ok((*id, row)) - } - -} - -impl Table { - fn new(table_schema: TableSchema) -> Self { - Self { - schema: table_schema, - rows: BTreeMap::new(), - indexes: HashMap::new(), - } - } - - fn attach_index(&mut self, column_position: ColumnPosition, column_index: ColumnIndex) { - self.indexes.insert(column_position, column_index); - } - - fn get_row_by_id(&self, id: UUID) -> Option { - self.rows.get(&id).cloned() - } - - fn get_rows_by_ids(&self, ids: HashSet) -> Vec { - ids.into_iter() - .filter_map(|id| self.get_row_by_id(id)) - .collect() - } - - fn get_rows_by_value(&self, column_position: ColumnPosition, value: &DbValue) -> Vec { - // brute-force search - self.rows.values() - .filter_map(|row| if row.get(column_position) == Some(value) { Some(row.clone()) } else { None }) - .collect() - } - - fn delete_row_by_id(&mut self, id: UUID) -> usize { - match self.rows.remove(&id) { - Some(row) => { - for (column_position, column_index) in &mut self.indexes { - if let DbValue::Indexable(value) = &row[*column_position] { - let _ = column_index.remove(value, id); - }; - } - 1 - }, - None => 0 - } - } - - fn delete_rows_by_ids(&mut self, ids: HashSet) -> usize { - let mut total_count = 0; - for id in ids { - total_count += self.delete_row_by_id(id) - } - total_count - } - - fn delete_rows_by_value(&mut self, column_position: ColumnPosition, value: &DbValue) -> usize { - let matched_ids: HashSet = self.rows.iter() - .filter_map(|(id, row)| if row.get(column_position) == Some(value) { Some(*id) } else { None }) - .collect(); - self.delete_rows_by_ids(matched_ids) - } - - fn select_where(&self, column_selection: ColumnSelection, condition: Option) -> DbResult> { - let selected_column_positions = self.schema.column_positions_from_column_selection(&column_selection)?; - match condition { - None => - // select all - Ok(self.rows.values().map(|row| restrict_columns(row, &selected_column_positions)).collect()), - - Some(Condition::Eq(eq_column_name, value)) => { - let eq_column_position = self.schema.column_position_from_column_name(&eq_column_name)?; - match value { - DbValue::Indexable(value) => { - match self.fetch_ids_from_index(eq_column_position, &value)? { - Some(ids) => - Ok(self.get_rows_by_ids(ids).iter().map(|row| restrict_columns(row, &selected_column_positions)).collect()), - None => - Ok(self.get_rows_by_value(eq_column_position, &DbValue::Indexable(value)).iter().map(|row| restrict_columns(row, &selected_column_positions)).collect()) - } - }, - _ => { - Ok(self.get_rows_by_value(eq_column_position, &value).iter().map(|row| restrict_columns(row, &selected_column_positions)).collect()) - } - } - } - } - } - - fn insert(&mut self, values: InsertionValues) -> DbResult<()> { - let (id, row) = self.schema.row_from_insertion_values(values)?; - - if self.rows.get(&id).is_some() { - return Err(Error::AttemptingToInsertAlreadyPresentId(self.schema.table_name.clone(), id)) - } - - for (column_position, column_index) in &mut self.indexes { - match row.get(*column_position) { - Some(DbValue::Indexable(val)) => { - column_index.add(val.clone(), id) - }, - Some(_) => {}, - None => return Err(Error::ColumnPositionDoesNotExist(self.schema.table_name.clone(), *column_position)) - } - } - - let _ = self.rows.insert(id, row); - Ok(()) - } - - fn delete_where(&mut self, maybe_condition: Option) -> DbResult { - match maybe_condition { - None => { - // delete all - let number_of_rows = self.rows.len(); - self.rows = BTreeMap::new(); - self.indexes = HashMap::new(); - Ok(number_of_rows) - }, - - Some(Condition::Eq(eq_column_name, value)) => { - let eq_column_position = self.schema.column_position_from_column_name(&eq_column_name)?; - match value { - DbValue::Indexable(value) => { - match self.fetch_ids_from_index(eq_column_position, &value)? { - Some(ids) => - Ok(self.delete_rows_by_ids(ids)), - None => - Ok(self.delete_rows_by_value(eq_column_position, &DbValue::Indexable(value))) - } - }, - _ => - Ok(self.delete_rows_by_value(eq_column_position, &value)) - } - } - } - } - - fn fetch_ids_from_index(&self, column_position: ColumnPosition, value: &IndexableDbValue) -> DbResult>> { - if self.schema.is_primary(column_position) { - match value { - IndexableDbValue::UUID(id) => - Ok(Some(HashSet::from([*id]))), - _ => { - let column_name: ColumnName = self.schema.column_name_from_column_position(column_position)?; - let type_ = self.schema.types[column_position]; - Err(Error::ValueDoesNotMatchExpectedType(self.schema.table_name.clone(), column_name, type_, DbValue::Indexable(value.clone()))) - } - } - } else { - match self.indexes.get(&column_position) { - Some(index) => { - let ids = index.get(value); - Ok(Some(ids)) - }, - None => { - Ok(None) - } - } - } - } -} - -impl ColumnIndex { - fn new() -> Self { - let index = BTreeMap::new(); - Self { index } - } - - fn get(&self, value: &IndexableDbValue) -> HashSet { - match self.index.get(value) { - Some(set) => set.clone(), - None => HashSet::new(), - } - } - - fn add(&mut self, value: IndexableDbValue, id: UUID) { - match self.index.get_mut(&value) { - Some(ids) => { - ids.insert(id); - }, - None => { - self.index.insert(value, HashSet::from([id])); - } - } - } - - // Should be used in the case when an indexed is created after the table has existed for a - // while. In such a case you need to build the index from the already existing rows. - fn update_from_table(&mut self, table: &Table, column_position: ColumnPosition) -> DbResult<()> { - for (id, row) in &table.rows { - let value = match row.get(column_position) { - Some(DbValue::Indexable(value)) => { - value.clone() - }, - Some(_) => { - let column_name: ColumnName = table.schema.column_name_from_column_position(column_position)?; - return Err(Error::AttemptToIndexNonIndexableColumn(table.schema.table_name.to_string(), column_name)) - }, - None => { - return Err(Error::ColumnPositionDoesNotExist(table.schema.table_name.to_string(), column_position)) - } - }; - self.add(value, *id) - } - Ok(()) - } - - fn remove(&mut self, value: &IndexableDbValue, id_to_be_removed: UUID) -> bool { - match self.index.get_mut(value) { - Some(ids) => { - let was_present = ids.remove(&id_to_be_removed); - was_present - }, - None => { - false - } - } - } -} - -#[derive(Debug)] -enum Response { - Selected(Vec), - Inserted, - Deleted(usize), // how many were deleted - TableCreated, - IndexCreated, -} - -type DbResult = Result; - -#[derive(Debug)] -enum Error { - TableDoesNotExist(TableName), - ColumnDoesNotExist(TableName, ColumnName), - ColumnPositionDoesNotExist(TableName, ColumnPosition), - ValueDoesNotMatchExpectedType(TableName, ColumnName, DbType, DbValue), - AttemptingToInsertAlreadyPresentId(TableName, UUID), - MissingTypeAnnotationOfColumn(TableName, ColumnPosition), - MissingColumnInInsertValues(TableName, ColumnName, InsertionValues), - MismatchBetweenInsertValuesAndColumns(TableName, InsertionValues), - AttemptToIndexNonIndexableColumn(TableName, ColumnName), -} +mod base; +mod table; +mod column_index; +mod operation; +mod interpreter; +mod error; +mod type_system; fn main() { - use DbValue::*; - use IndexableDbValue::*; - use Operation::*; - use ColumnSelection::*; - use Condition::*; - - let users_schema = { - let id: ColumnPosition = 0; - let name: ColumnPosition = 1; - let age: ColumnPosition = 2; - - TableSchema { - table_name: "users".to_string(), - primary_key: id, - 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], - } - }; - let users = users_schema.table_name.clone(); - - - let mut state = State::new(); - state.interpret(Operation::CreateTable(users.clone(), users_schema)).unwrap(); - - let (id0, name0, age0) = ( - Indexable(UUID(0)), - Indexable(String("Plato".to_string())), - Indexable(Int(64)) - ); - println!("==INSERT Plato=="); - 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)) - ); - println!("==INSERT Aristotle=="); - state.interpret(Insert(users.clone(), vec![ - ("id".to_string(), id1.clone()), - ("name".to_string(), name1.clone()), - ("age".to_string(), age1.clone()), - ])).unwrap(); - println!(); - - { - let response: Response = state.interpret(Operation::Select(users.clone(), ColumnSelection::All, None)).unwrap(); - println!("==SELECT ALL=="); - println!("{:?}", response); - println!(); - } - { - let response: Response = state.interpret(Select(users.clone(), All, Some(Eq("id".to_string(), id0.clone())))).unwrap(); - println!("==SELECT Plato=="); - println!("{:?}", response); - println!(); - } - - { - let delete_response: Response = state.interpret(Delete(users.clone(), Some(Eq("id".to_string(), id0.clone())))).unwrap(); - println!("==DELETE Plato=="); - let response: Response = state.interpret(Select(users.clone(), Columns(vec!["name".to_string(), "id".to_string()]), None)).unwrap(); - println!("==SELECT All=="); - println!("{:?}", response); - println!(); - } - -} - - -#[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: id, - 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 (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); - } - } - - #[test] - fn test_delete() { - 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 (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 delete_response: Response = state.interpret(Delete(users.clone(), Some(Eq("id".to_string(), id0.clone())))).unwrap(); - assert!(matches!(delete_response, Response::Deleted(1))); - - 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() == 1); - let row = &rows[0]; - - assert!(row.len() == 3); - assert!(row[0] == id1); - assert!(row[1] == name1); - assert!(row[2] == age1); - } - - // TODO: Test CreateIndex + interpreter::example(); } diff --git a/minisql/src/operation.rs b/minisql/src/operation.rs new file mode 100644 index 0000000..7235fed --- /dev/null +++ b/minisql/src/operation.rs @@ -0,0 +1,42 @@ +use crate::base::{TableName, ColumnName}; +use crate::type_system::DbValue; +use crate::base::TableSchema; + +// ==============SQL operations================ +// 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` +pub enum Operation { + Select(TableName, ColumnSelection, Option), + Insert(TableName, InsertionValues), + Delete(TableName, Option), + // Update(...), + CreateTable(TableName, TableSchema), + CreateIndex(TableName, ColumnName), + // DropTable(TableName), +} + +pub type InsertionValues = Vec<(ColumnName, DbValue)>; + +pub enum ColumnSelection { + All, + Columns(Vec), +} + +pub enum Condition { + // And(Box, Box), + // Or(Box, Box), + // Not(Box), + + Eq(ColumnName, DbValue), + // LessOrEqual(ColumnName, DbValue), + // Less(ColumnName, DbValue), + + // StringCondition(StringCondition), +} + +// enum StringCondition { +// Prefix(ColumnName, String), +// Substring(ColumnName, String), +// } diff --git a/minisql/src/table.rs b/minisql/src/table.rs new file mode 100644 index 0000000..a6bf175 --- /dev/null +++ b/minisql/src/table.rs @@ -0,0 +1,203 @@ +use std::collections::{BTreeMap, HashMap, HashSet}; + +use crate::base::{TableSchema, ColumnPosition, ColumnName, DbResult}; +use crate::type_system::{UUID, DbValue, IndexableDbValue}; +use crate::column_index::ColumnIndex; +use crate::operation::{Condition, ColumnSelection, InsertionValues}; +use crate::error::Error; + + +#[derive(Debug)] +pub struct Table { + pub schema: TableSchema, + pub rows: Rows, // TODO: Consider wrapping this in a lock. Also consider if we need to have the + // same lock for both rows and indexes + pub indexes: + HashMap // TODO: Consider generalizing `ColumnPosition` to something that would also apply to a pair of `ColumnNames` etc +} + + +// Use `ColumnPosition` as index +pub type Row = Vec; + +pub type Rows = + // TODO: This should be some sort of an interface to a dictionary + // s.t. in the background it may modify stuff in memory or talk to the disk + BTreeMap; + + // interface + // insert(id, value) + +fn restrict_columns(row: &Row, columns: &Vec) -> Row { + // If the index from `columns` is non-existant in `row`, it will just ignore it. + let mut subrow: Row = vec![]; + for column_position in columns { + match row.get(*column_position) { + Some(value) => { + subrow.push(value.clone()) + }, + None => {} + } + } + subrow +} + +impl Table { + pub fn new(table_schema: TableSchema) -> Self { + Self { + schema: table_schema, + rows: BTreeMap::new(), + indexes: HashMap::new(), + } + } + + pub fn attach_index(&mut self, column_position: ColumnPosition, column_index: ColumnIndex) { + self.indexes.insert(column_position, column_index); + } + + fn get_row_by_id(&self, id: UUID) -> Option { + self.rows.get(&id).cloned() + } + + fn get_rows_by_ids(&self, ids: HashSet) -> Vec { + ids.into_iter() + .filter_map(|id| self.get_row_by_id(id)) + .collect() + } + + fn get_rows_by_value(&self, column_position: ColumnPosition, value: &DbValue) -> Vec { + // brute-force search + self.rows.values() + .filter_map(|row| if row.get(column_position) == Some(value) { Some(row.clone()) } else { None }) + .collect() + } + + fn delete_row_by_id(&mut self, id: UUID) -> usize { + match self.rows.remove(&id) { + Some(row) => { + for (column_position, column_index) in &mut self.indexes { + if let DbValue::Indexable(value) = &row[*column_position] { + let _ = column_index.remove(value, id); + }; + } + 1 + }, + None => 0 + } + } + + fn delete_rows_by_ids(&mut self, ids: HashSet) -> usize { + let mut total_count = 0; + for id in ids { + total_count += self.delete_row_by_id(id) + } + total_count + } + + fn delete_rows_by_value(&mut self, column_position: ColumnPosition, value: &DbValue) -> usize { + let matched_ids: HashSet = self.rows.iter() + .filter_map(|(id, row)| if row.get(column_position) == Some(value) { Some(*id) } else { None }) + .collect(); + self.delete_rows_by_ids(matched_ids) + } + + pub fn select_where(&self, column_selection: ColumnSelection, condition: Option) -> DbResult> { + let selected_column_positions = self.schema.column_positions_from_column_selection(&column_selection)?; + match condition { + None => + // select all + Ok(self.rows.values().map(|row| restrict_columns(row, &selected_column_positions)).collect()), + + Some(Condition::Eq(eq_column_name, value)) => { + let eq_column_position = self.schema.column_position_from_column_name(&eq_column_name)?; + match value { + DbValue::Indexable(value) => { + match self.fetch_ids_from_index(eq_column_position, &value)? { + Some(ids) => + Ok(self.get_rows_by_ids(ids).iter().map(|row| restrict_columns(row, &selected_column_positions)).collect()), + None => + Ok(self.get_rows_by_value(eq_column_position, &DbValue::Indexable(value)).iter().map(|row| restrict_columns(row, &selected_column_positions)).collect()) + } + }, + _ => { + Ok(self.get_rows_by_value(eq_column_position, &value).iter().map(|row| restrict_columns(row, &selected_column_positions)).collect()) + } + } + } + } + } + + pub fn insert(&mut self, values: InsertionValues) -> DbResult<()> { + let (id, row) = self.schema.row_from_insertion_values(values)?; + + if self.rows.get(&id).is_some() { + return Err(Error::AttemptingToInsertAlreadyPresentId(self.schema.table_name.clone(), id)) + } + + for (column_position, column_index) in &mut self.indexes { + match row.get(*column_position) { + Some(DbValue::Indexable(val)) => { + column_index.add(val.clone(), id) + }, + Some(_) => {}, + None => return Err(Error::ColumnPositionDoesNotExist(self.schema.table_name.clone(), *column_position)) + } + } + + let _ = self.rows.insert(id, row); + Ok(()) + } + + // TODO: Split into delete all and delete_where(condition) + pub fn delete_where(&mut self, maybe_condition: Option) -> DbResult { + match maybe_condition { + None => { + // delete all + let number_of_rows = self.rows.len(); + self.rows = BTreeMap::new(); + self.indexes = HashMap::new(); + Ok(number_of_rows) + }, + + Some(Condition::Eq(eq_column_name, value)) => { + let eq_column_position = self.schema.column_position_from_column_name(&eq_column_name)?; + match value { + DbValue::Indexable(value) => { + match self.fetch_ids_from_index(eq_column_position, &value)? { + Some(ids) => + Ok(self.delete_rows_by_ids(ids)), + None => + Ok(self.delete_rows_by_value(eq_column_position, &DbValue::Indexable(value))) + } + }, + _ => + Ok(self.delete_rows_by_value(eq_column_position, &value)) + } + } + } + } + + fn fetch_ids_from_index(&self, column_position: ColumnPosition, value: &IndexableDbValue) -> DbResult>> { + if self.schema.is_primary(column_position) { + match value { + IndexableDbValue::UUID(id) => + Ok(Some(HashSet::from([*id]))), + _ => { + let column_name: ColumnName = self.schema.column_name_from_column_position(column_position)?; + let type_ = self.schema.types[column_position]; + Err(Error::ValueDoesNotMatchExpectedType(self.schema.table_name.clone(), column_name, type_, DbValue::Indexable(value.clone()))) + } + } + } else { + match self.indexes.get(&column_position) { + Some(index) => { + let ids = index.get(value); + Ok(Some(ids)) + }, + None => { + Ok(None) + } + } + } + } +} diff --git a/minisql/src/type_system.rs b/minisql/src/type_system.rs new file mode 100644 index 0000000..15c46b9 --- /dev/null +++ b/minisql/src/type_system.rs @@ -0,0 +1,43 @@ +// ==============Types================ +#[derive(Debug, Clone, Copy)] +pub enum DbType { + String, + Int, + Number, + UUID, +} + +// ==============Values================ +pub type UUID = u64; + +// TODO: What about nulls? I would rather not have that in SQL, it sucks. +// I would rather have non-nullable values by default, +// and something like an explicit Option type for nulls. +#[derive(Debug, Clone, PartialEq)] +pub enum DbValue { + Number(f64), // TODO: Can't put floats as keys in maps, since they don't implement Eq. What to + // do? + Indexable(IndexableDbValue), +} + +#[derive(Debug, Ord, Eq, Clone, PartialOrd, PartialEq)] +pub enum IndexableDbValue { + String(String), + Int(u64), + UUID(UUID), + // TODO: what about null? +} + +impl DbValue { + fn to_type(self) -> DbType { + match self { + Self::Number(_) => DbType::Number, + Self::Indexable(val) => + match val { + IndexableDbValue::String(_) => DbType::String, + IndexableDbValue::Int(_) => DbType::Int, + IndexableDbValue::UUID(_) => DbType::UUID, + } + } + } +}