diff --git a/Cargo.lock b/Cargo.lock index 5c4af8c..8b89a08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,6 +55,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "bimap" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" + [[package]] name = "bincode" version = "2.0.0-rc.3" @@ -147,6 +153,9 @@ checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "minisql" version = "0.1.0" +dependencies = [ + "bimap", +] [[package]] name = "miniz_oxide" diff --git a/minisql/Cargo.toml b/minisql/Cargo.toml index df68143..1a108f1 100644 --- a/minisql/Cargo.toml +++ b/minisql/Cargo.toml @@ -6,3 +6,4 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +bimap = "0.6.3" diff --git a/minisql/src/error.rs b/minisql/src/error.rs new file mode 100644 index 0000000..51ca0cf --- /dev/null +++ b/minisql/src/error.rs @@ -0,0 +1,17 @@ +use crate::internals::row::ColumnPosition; +use crate::internals::schema::{ColumnName, TableName}; +use crate::operation::InsertionValues; +use crate::type_system::{DbType, Uuid, Value}; + +#[derive(Debug)] +pub enum Error { + TableDoesNotExist(TableName), + ColumnDoesNotExist(TableName, ColumnName), + ColumnPositionDoesNotExist(TableName, ColumnPosition), + ValueDoesNotMatchExpectedType(TableName, ColumnName, DbType, Value), + AttemptingToInsertAlreadyPresentId(TableName, Uuid), + MissingTypeAnnotationOfColumn(TableName, ColumnPosition), + MissingColumnInInsertValues(TableName, ColumnName, InsertionValues), + MismatchBetweenInsertValuesAndColumns(TableName, InsertionValues), + AttemptToIndexNonIndexableColumn(TableName, ColumnName), +} diff --git a/minisql/src/internals/column_index.rs b/minisql/src/internals/column_index.rs new file mode 100644 index 0000000..cdd331d --- /dev/null +++ b/minisql/src/internals/column_index.rs @@ -0,0 +1,38 @@ +use crate::type_system::{IndexableValue, Uuid}; +use std::collections::{BTreeMap, HashSet}; + +#[derive(Debug)] +pub struct ColumnIndex { + index: BTreeMap>, +} + +impl ColumnIndex { + pub fn new() -> Self { + let index = BTreeMap::new(); + Self { index } + } + + pub fn get(&self, value: &IndexableValue) -> Option<&HashSet> { + self.index.get(value) + } + + pub fn add(&mut self, value: IndexableValue, id: Uuid) { + match self.index.get_mut(&value) { + Some(ids) => { + ids.insert(id); + } + None => { + self.index.insert(value, HashSet::from([id])); + } + } + } + + pub fn remove(&mut self, value: &IndexableValue, id_to_be_removed: Uuid) -> bool { + match self.index.get_mut(value) { + Some(ids) => { + ids.remove(&id_to_be_removed) // true if was present + } + None => false, + } + } +} diff --git a/minisql/src/internals/mod.rs b/minisql/src/internals/mod.rs new file mode 100644 index 0000000..864d9d7 --- /dev/null +++ b/minisql/src/internals/mod.rs @@ -0,0 +1,4 @@ +pub mod column_index; +pub mod row; +pub mod schema; +pub mod table; diff --git a/minisql/src/internals/row.rs b/minisql/src/internals/row.rs new file mode 100644 index 0000000..ad8dc1e --- /dev/null +++ b/minisql/src/internals/row.rs @@ -0,0 +1,71 @@ +use crate::type_system::Value; +use std::ops::{Index, IndexMut}; +use std::slice::SliceIndex; + +pub type ColumnPosition = usize; + +#[derive(Debug, Clone)] +pub struct Row(Vec); + +impl Index for Row +where + Idx: SliceIndex<[Value]>, +{ + type Output = Idx::Output; + + fn index(&self, index: Idx) -> &Self::Output { + &self.0[index] + } +} + +impl IndexMut for Row +where + Idx: SliceIndex<[Value]>, +{ + fn index_mut(&mut self, index: Idx) -> &mut Self::Output { + &mut self.0[index] + } +} + +impl FromIterator for Row { + fn from_iter>(iter: I) -> Self { + let mut v = vec![]; + for x in iter { + v.push(x) + } + Row(v) + } +} + +impl Row { + pub fn new() -> Self { + Row(vec![]) + } + + pub fn with_number_of_columns(number_of_columns: usize) -> Self { + Row(Vec::with_capacity(number_of_columns)) + } + + pub fn push(&mut self, value: Value) { + self.0.push(value) + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn get(&self, column_position: ColumnPosition) -> Option<&Value> { + self.0.get(column_position) + } + + pub fn restrict_columns(&self, columns: &Vec) -> Row { + // If the index from `columns` is non-existant in `row`, it will just ignore it. + let mut subrow: Row = Row::new(); + for column_position in columns { + if let Some(value) = self.get(*column_position) { + subrow.0.push(value.clone()) + } + } + subrow + } +} diff --git a/minisql/src/internals/schema.rs b/minisql/src/internals/schema.rs new file mode 100644 index 0000000..c23c24a --- /dev/null +++ b/minisql/src/internals/schema.rs @@ -0,0 +1,162 @@ +use crate::error::Error; +use crate::internals::row::{ColumnPosition, Row}; +use crate::operation::{ColumnSelection, InsertionValues}; +use crate::result::DbResult; +use crate::type_system::{DbType, IndexableValue, Uuid, Value}; +use bimap::BiMap; +use std::collections::HashMap; + +// 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 { + 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; + +impl TableSchema { + pub fn new(table_name: TableName, primary_key: ColumnPosition, column_name_position_map: Vec<(ColumnName, ColumnPosition)>, types: Vec) -> Self { + let mut column_name_position_mapping: BiMap = BiMap::new(); + for (column_name, column_position) in column_name_position_map { + column_name_position_mapping.insert(column_name, column_position); + } + Self { table_name, primary_key, column_name_position_mapping, types } + } + + pub fn table_name(&self) -> &TableName { + &self.table_name + } + + 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() + } + + 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 = Row::with_number_of_columns(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: Uuid = match row.get(self.primary_key) { + Some(Value::Indexable(IndexableValue::Uuid(id))) => *id, + Some(_) => unreachable!(), + None => unreachable!(), + }; + + Ok((id, row)) + } +} diff --git a/minisql/src/internals/table.rs b/minisql/src/internals/table.rs new file mode 100644 index 0000000..e5506e0 --- /dev/null +++ b/minisql/src/internals/table.rs @@ -0,0 +1,262 @@ +use std::collections::{BTreeMap, HashMap, HashSet}; + +use crate::error::Error; +use crate::internals::column_index::ColumnIndex; +use crate::internals::row::{ColumnPosition, Row}; +use crate::internals::schema::{ColumnName, TableSchema, TableName}; +use crate::result::DbResult; +use crate::type_system::{IndexableValue, Uuid, Value}; + +#[derive(Debug)] +pub 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, +} + +pub type Rows = BTreeMap; + +impl Table { + pub fn new(table_schema: TableSchema) -> Self { + Self { + schema: table_schema, + rows: BTreeMap::new(), + indexes: HashMap::new(), + } + } + + pub fn schema(&self) -> &TableSchema { + &self.schema + } + + pub fn rows(&self) -> &Rows { + &self.rows + } + + pub fn indexes(&self) -> &HashMap { + &self.indexes + } + + pub fn table_name(&self) -> &TableName { + &self.schema.table_name() + } + + // ======Selection====== + 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: &Value) -> Vec { + // brute-force search + self.rows + .values() + .filter_map(|row| { + if row.get(column_position) == Some(value) { + Some(row.clone()) + } else { + None + } + }) + .collect() + } + + pub fn select_all_rows(&self, selected_column_positions: &Vec) -> Vec { + self.rows + .values() + .map(|row| row.restrict_columns(selected_column_positions)) + .collect() + } + + pub fn select_rows_where_eq( + &self, + selected_column_positions: &Vec, + column_position: ColumnPosition, + value: Value, + ) -> DbResult> { + match value { + Value::Indexable(value) => match self.fetch_ids_from_index(column_position, &value)? { + Some(ids) => Ok(self + .get_rows_by_ids(ids) + .iter() + .map(|row| row.restrict_columns(selected_column_positions)) + .collect()), + None => Ok(self + .get_rows_by_value(column_position, &Value::Indexable(value)) + .iter() + .map(|row| row.restrict_columns(selected_column_positions)) + .collect()), + }, + _ => Ok(self + .get_rows_by_value(column_position, &value) + .iter() + .map(|row| row.restrict_columns(selected_column_positions)) + .collect()), + } + } + + // ======Insertion====== + pub fn insert_row_at(&mut self, id: Uuid, row: Row) -> DbResult<()> { + if self.rows.get(&id).is_some() { + return Err(Error::AttemptingToInsertAlreadyPresentId( + self.table_name().clone(), + id, + )); + } + + for (column_position, column_index) in &mut self.indexes { + match row.get(*column_position) { + Some(Value::Indexable(val)) => column_index.add(val.clone(), id), + Some(_) => {} + None => { + return Err(Error::ColumnPositionDoesNotExist( + self.schema.table_name().clone(), // Note that I can't simply use self.table_name() here because of rust borrowing rules. + *column_position, + )) + } + } + } + + let _ = self.rows.insert(id, row); + Ok(()) + } + + // ======Deletion====== + 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 Value::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: &Value) -> 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 delete_all_rows(&mut self) -> usize { + let number_of_rows = self.rows.len(); + self.rows = BTreeMap::new(); + self.indexes = HashMap::new(); + number_of_rows + } + + pub fn delete_rows_where_eq( + &mut self, + column_position: ColumnPosition, + value: Value, + ) -> DbResult { + match value { + Value::Indexable(value) => match self.fetch_ids_from_index(column_position, &value)? { + Some(ids) => Ok(self.delete_rows_by_ids(ids)), + None => Ok(self.delete_rows_by_value(column_position, &Value::Indexable(value))), + }, + _ => Ok(self.delete_rows_by_value(column_position, &value)), + } + } + + // ======Indexing====== + pub fn attach_index(&mut self, column_position: ColumnPosition) -> DbResult<()> { + let mut column_index: ColumnIndex = ColumnIndex::new(); + update_index_from_table(&mut column_index, self, column_position)?; + self.indexes.insert(column_position, column_index); + Ok(()) + } + + fn fetch_ids_from_index( + &self, + column_position: ColumnPosition, + value: &IndexableValue, + ) -> DbResult>> { + if self.schema.is_primary(column_position) { + match value { + IndexableValue::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.table_name().clone(), + column_name, + type_, + Value::Indexable(value.clone()), + )) + } + } + } else { + match self.indexes.get(&column_position) { + Some(index) => { + // Note that we are cloning the ids here! This can be very wasteful in some cases. + // It would be possible to just return a reference, + // but this seems fairly non-trivial. + let ids = index.get(value).cloned(); + Ok(ids) + } + None => Ok(None), + } + } + } +} + +// Should be used in the case when an index 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_index_from_table( + column_index: &mut ColumnIndex, + table: &Table, + column_position: ColumnPosition, +) -> DbResult<()> { + for (id, row) in &table.rows { + let value = match row.get(column_position) { + Some(Value::Indexable(value)) => value.clone(), + Some(_) => { + let column_name: ColumnName = table + .schema + .column_name_from_column_position(column_position)?; + return Err(Error::AttemptToIndexNonIndexableColumn( + table.table_name().to_string(), + column_name, + )); + } + None => { + return Err(Error::ColumnPositionDoesNotExist( + table.table_name().to_string(), + column_position, + )) + } + }; + column_index.add(value, *id) + } + Ok(()) +} diff --git a/minisql/src/interpreter.rs b/minisql/src/interpreter.rs new file mode 100644 index 0000000..4bd3f35 --- /dev/null +++ b/minisql/src/interpreter.rs @@ -0,0 +1,621 @@ +use crate::error::Error; +use crate::internals::row::{ColumnPosition, Row}; +use crate::internals::schema::{TableName, TableSchema}; +use crate::internals::table::Table; +use crate::operation::{ColumnSelection, Condition, Operation}; +use crate::result::DbResult; +use crate::type_system::{DbType, IndexableValue, Value}; +use bimap::BiMap; + +// 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 + pub 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)?; + + let selected_column_positions: Vec = table + .schema() + .column_positions_from_column_selection(&column_selection)?; + let selected_rows = match maybe_condition { + None => table.select_all_rows(&selected_column_positions), + + Some(Condition::Eq(eq_column_name, value)) => { + let eq_column_position = table + .schema() + .column_position_from_column_name(&eq_column_name)?; + table.select_rows_where_eq( + &selected_column_positions, + eq_column_position, + value, + )? + } + }; + + Ok(Response::Selected(selected_rows)) + } + Insert(table_name, values) => { + let table: &mut Table = self.table_from_name_mut(&table_name)?; + + let (id, row) = table.schema().row_from_insertion_values(values)?; + table.insert_row_at(id, row)?; + Ok(Response::Inserted) + } + Delete(table_name, maybe_condition) => { + let table: &mut Table = self.table_from_name_mut(&table_name)?; + + let rows_affected = match maybe_condition { + None => table.delete_all_rows(), + Some(Condition::Eq(eq_column_name, value)) => { + let eq_column_position = table + .schema() + .column_position_from_column_name(&eq_column_name)?; + table.delete_rows_where_eq(eq_column_position, value)? + } + }; + + 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)?; + + table.attach_index(column_position)?; + 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::*; + use std::collections::HashSet; + + fn users_schema() -> TableSchema { + let id: ColumnPosition = 0; + let name: ColumnPosition = 1; + let age: ColumnPosition = 2; + + TableSchema::new( + "users".to_string(), + id, + vec!( + ("id".to_string(), id), + ("name".to_string(), name), + ("age".to_string(), age), + ), + 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.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 IndexableValue::*; + use Value::*; + + 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 ColumnSelection::*; + use Condition::*; + use IndexableValue::*; + use Operation::*; + use Value::*; + + 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 ColumnSelection::*; + use Condition::*; + use IndexableValue::*; + use Operation::*; + use Value::*; + + 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); + } + + #[test] + fn test_index() { + use IndexableValue::*; + use Operation::*; + use Value::*; + + 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(); + + state + .interpret(CreateIndex(users.clone(), "name".to_string())) + .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(); + + assert!(state.tables.len() == 1); + let table = &state.tables[0]; + assert!(table.rows().len() == 2); + + let user: ColumnPosition = 1; + assert!(table.indexes().contains_key(&user)); + + let index = table.indexes().get(&user).unwrap(); + + let plato_id = 0; + let aristotle_id = 1; + + let plato_ids = index.get(&String("Plato".to_string())).cloned().unwrap_or(HashSet::new()); + assert!(plato_ids.contains(&plato_id)); + assert!(!plato_ids.contains(&aristotle_id)); + assert!(plato_ids.len() == 1); + } +} + +pub fn example() { + use ColumnSelection::*; + use Condition::*; + use IndexableValue::*; + use Operation::*; + use Value::*; + + let users_schema: TableSchema = { + let id: ColumnPosition = 0; + let name: ColumnPosition = 1; + let age: ColumnPosition = 2; + + TableSchema::new( + "users".to_string(), + id, + vec!( + ("id".to_string(), id), + ("name".to_string(), name), + ("age".to_string(), age), + ), + 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 31b1c7b..9a4f292 100644 --- a/minisql/src/main.rs +++ b/minisql/src/main.rs @@ -1,259 +1,10 @@ -use std::collections::{BTreeMap, HashMap}; - -// ==============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. -enum Operation { - Select(TableName, ColumnSelection, Option), - Insert(TableName, InsertionValues), - Delete(TableName, Option), - // Update(...), - CreateTable(TableName, TableSchema), - CreateIndex(TableName, ColumnName), // TODO: Is this sufficient? - // 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 as in SQL, it sucks. -// I would rather have non-nullable values by default, -// and something like an explicit Option type for nulls. -enum DbValue { - String(String), - Int(u64), - Number(f64), - UUID(UUID), - // TODO: what bout null? -} - -// TODO: Can this be autogenerated from the values? -enum DbType { - String, - Int, - Number, - UUID, -} - -impl DbValue { - // TODO: Can this be autogenerated? - fn to_type(self) -> DbType { - match self { - Self::String(_) => DbType::String, - Self::Int(_) => DbType::Int, - Self::Number(_) => DbType::Number, - Self::UUID(_) => DbType::UUID, - } - } -} - - -// ==============Tables================ -// table-metadata and data - -type TableName = String; -type TablePosition = u32; - -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 -} - -// TODO: Is this really indexed by DbValues? -// Maybe we should have a separate index type for each type of value we're indexing over -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. -struct TableSchema { - columns: HashMap -} - -// TODO -fn column_position(table_meta: TableSchema, column_name: ColumnName) -> Option { - todo!() -} - -// Use `TablePosition` as index -type Tables = Vec
; - - -type ColumnName = String; -type ColumnPosition = u32; - -// 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) - -// ==============Interpreter================ -struct State { - table_positions: HashMap, - tables: Vec
, -} - -impl State { - fn table_from_name<'b: 'a, 'a>(&'b self, table_name: TableName) -> Option<&'a Table> { - todo!() - } - - fn attach_table(&mut self, table: Table) { - todo!() - } -} - -// TODO: Give a better name to something that you can respond to with rows -trait SqlConsumer { - // TODO: -} - -// TODO: This should return a reference to the table -// 'tables_life contains 'table_life -fn get_table<'tables_life: 'table_life, 'table_life>(tables: &'tables_life Tables, table_name: &TableName) -> &'table_life Table { - // let table_position: - 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) -> () { - // TODO: lock stuff - use Operation::*; - - match operation { - Select(table_name, column_selection, maybe_condition) => { - let table: &Table = todo!(); - table.select_where(column_selection, maybe_condition, consumer) - }, - Insert(table_name, values) => { - let table: &mut Table = todo!(); - - table.insert(values, consumer) - }, - Delete(table_name, maybe_condition) => { - let table: &mut Table = todo!(); - - table.delete_where(maybe_condition, consumer) - }, - CreateTable(table_name, table_schema) => { - let table = Table::new(table_name, table_schema); - state.attach_table(table); - todo!() - }, - CreateIndex(table_name, column_name) => { - let table: &mut Table = todo!(); - - let index: ColumnIndex = ColumnIndex::new(table, column_name); - table.attach_index(index); - }, // TODO: Is this sufficient? - // - } -} - -impl ColumnIndex { - fn new(table: &Table, column_name: ColumnName) -> ColumnIndex { - todo!() - } -} - - -impl Table { - fn new(table_name: TableName, table_schema: TableSchema) -> Table { - todo!() - } - - fn attach_index(&mut self, column_index: ColumnIndex) { - todo!() - } - - fn select_where(&self, column_selection: ColumnSelection, maybe_condition: Option, consumer: impl SqlConsumer) { - match maybe_condition { - None => { - // .iter() will give us an iterator over all the rows - - // two choices - // 1. optimized version - // self.iter_with_columns(column_selection).for_each(|row| { - // consumer.send(row) - // }); - // 2. - // self.iter() - // .map(|row| row.select_columns(column_selection)) - // .for_each(|reduced_row| { - // consumer.send(row) - // }); - todo!() - }, - Some(Condition::Eq(column_name, value)) => { - // is column_name primary key? then it is easy - // self.get(id) - // is column_name indexed? Then get the index, and then it is not easy, because you - // may get a set of ids. - // what if it is not primary nor indexed? then you need to brute force your way - // through the whole table? - todo!() - } - } - } - - fn insert(&mut self, values: InsertionValues, consumer: impl SqlConsumer) { - // 1. You need to update indices - // 2. you simply insert the data - todo!() - } - - fn delete_where(&mut self, maybe_condition: Option, consumer: impl SqlConsumer) { - // kinda similar to select with respect to the conditions - // update index - todo!() - } -} - -// enum Response { -// Selected(impl Iter), // TODO: How to do this? Some reference to an iterator somehow... slice..? -// Inserted(???), -// Deleted(usize), // how many were deleted -// } +mod error; +mod internals; +mod interpreter; +mod operation; +mod result; +mod type_system; fn main() { - println!("Hello, world!"); + interpreter::example(); } diff --git a/minisql/src/operation.rs b/minisql/src/operation.rs new file mode 100644 index 0000000..a5fd0b7 --- /dev/null +++ b/minisql/src/operation.rs @@ -0,0 +1,40 @@ +use crate::internals::schema::{ColumnName, TableName, TableSchema}; +use crate::type_system::Value; + +// ==============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, Value)>; + +pub enum ColumnSelection { + All, + Columns(Vec), +} + +pub enum Condition { + // And(Box, Box), + // Or(Box, Box), + // Not(Box), + Eq(ColumnName, Value), + // LessOrEqual(ColumnName, DbValue), + // Less(ColumnName, DbValue), + + // StringCondition(StringCondition), +} + +// enum StringCondition { +// Prefix(ColumnName, String), +// Substring(ColumnName, String), +// } diff --git a/minisql/src/result.rs b/minisql/src/result.rs new file mode 100644 index 0000000..fcad8b5 --- /dev/null +++ b/minisql/src/result.rs @@ -0,0 +1,3 @@ +use crate::error::Error; + +pub type DbResult = Result; diff --git a/minisql/src/type_system.rs b/minisql/src/type_system.rs new file mode 100644 index 0000000..4edc3ec --- /dev/null +++ b/minisql/src/type_system.rs @@ -0,0 +1,42 @@ +// ==============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 Value { + Number(f64), // TODO: Can't put floats as keys in maps, since they don't implement Eq. What to + // do? + Indexable(IndexableValue), +} + +#[derive(Debug, Ord, Eq, Clone, PartialOrd, PartialEq)] +pub enum IndexableValue { + String(String), + Int(u64), + Uuid(Uuid), + // TODO: what about null? +} + +impl Value { + pub fn to_type(&self) -> DbType { + match self { + Self::Number(_) => DbType::Number, + Self::Indexable(val) => match val { + IndexableValue::String(_) => DbType::String, + IndexableValue::Int(_) => DbType::Int, + IndexableValue::Uuid(_) => DbType::Uuid, + }, + } + } +}