diff --git a/minisql/src/error.rs b/minisql/src/error.rs index 451ad7a..2c66f3f 100644 --- a/minisql/src/error.rs +++ b/minisql/src/error.rs @@ -1,16 +1,11 @@ use std::num::{ParseFloatError, ParseIntError}; use std::str::Utf8Error; use thiserror::Error; -use crate::internals::row::ColumnPosition; use crate::schema::{ColumnName, TableName}; -use crate::type_system::{DbType, Uuid, Value}; +use crate::type_system::Uuid; #[derive(Debug, Error)] -pub enum Error { - #[error("column position {1} of table {0} does not exist")] - ColumnPositionDoesNotExist(TableName, ColumnPosition), - #[error("column {1} of table {0} has unexpected type {2:?} and value {3:?}")] - ValueDoesNotMatchExpectedType(TableName, ColumnName, DbType, Value), +pub enum RuntimeError { #[error("table {0} already contains row with id {1}")] AttemptingToInsertAlreadyPresentId(TableName, Uuid), #[error("table {0} cannot be indexed on column {1}")] diff --git a/minisql/src/internals/table.rs b/minisql/src/internals/table.rs index 05ee66e..b77650d 100644 --- a/minisql/src/internals/table.rs +++ b/minisql/src/internals/table.rs @@ -1,6 +1,6 @@ use std::collections::{BTreeMap, HashMap, HashSet}; -use crate::error::Error; +use crate::error::RuntimeError; use crate::internals::column_index::ColumnIndex; use crate::internals::row::{ColumnPosition, Row}; use crate::restricted_row::RestrictedRow; @@ -108,22 +108,16 @@ impl Table { // ======Insertion====== pub fn insert_row_at(&mut self, id: Uuid, row: Row) -> DbResult<()> { if self.rows.get(&id).is_some() { - return Err(Error::AttemptingToInsertAlreadyPresentId( + return Err(RuntimeError::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, - )) - } + for (column, column_index) in &mut self.indexes { + match &row[*column] { + Value::Indexable(val) => column_index.add(val.clone(), id), + _ => {}, } } @@ -206,19 +200,7 @@ impl Table { if self.schema.is_primary(column_position) { match value { IndexableValue::Uuid(id) => Ok(Some(HashSet::from([*id]))), - _ => { - // TODO: This validation step is not really necessary. - let column_name: ColumnName = self - .schema - .column_name_from_column_position(column_position)?; - let type_ = self.schema.column_type(column_position); - Err(Error::ValueDoesNotMatchExpectedType( - self.table_name().clone(), - column_name, - type_, - Value::Indexable(value.clone()), - )) - } + _ => unreachable!() // SAFETY: Validation guarantees primary column has correct Uuid type. } } else { match self.indexes.get(&column_position) { @@ -240,26 +222,21 @@ impl Table { fn update_index_from_table( column_index: &mut ColumnIndex, table: &Table, - column_position: ColumnPosition, + column: ColumnPosition, ) -> DbResult<()> { for (id, row) in &table.rows { - let value = match row.get(column_position) { - Some(Value::Indexable(value)) => value.clone(), - Some(_) => { + let value = match &row[column] { + Value::Indexable(value) => value.clone(), + _ => { let column_name: ColumnName = table .schema - .column_name_from_column_position(column_position)?; - return Err(Error::AttemptToIndexNonIndexableColumn( + .column_name_from_column(column); + // TODO: Perhaps this should be handled in validation? + return Err(RuntimeError::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) } diff --git a/minisql/src/operation.rs b/minisql/src/operation.rs index dae8718..5aff265 100644 --- a/minisql/src/operation.rs +++ b/minisql/src/operation.rs @@ -4,7 +4,7 @@ use crate::internals::row::ColumnPosition; use crate::interpreter::TablePosition; // Validated operation. Constructed by validation crate. -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum Operation { Select(TablePosition, ColumnSelection, Option), Insert(TablePosition, InsertionValues), @@ -13,11 +13,12 @@ pub enum Operation { CreateIndex(TablePosition, ColumnPosition), } +// Assumes that these are sorted by column position. pub type InsertionValues = Vec; pub type ColumnSelection = Vec; -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum Condition { Eq(ColumnPosition, Value), } diff --git a/minisql/src/result.rs b/minisql/src/result.rs index fcad8b5..ace48fe 100644 --- a/minisql/src/result.rs +++ b/minisql/src/result.rs @@ -1,3 +1,3 @@ -use crate::error::Error; +use crate::error::RuntimeError; -pub type DbResult = Result; +pub type DbResult = Result; diff --git a/minisql/src/schema.rs b/minisql/src/schema.rs index 97cd87b..0230220 100644 --- a/minisql/src/schema.rs +++ b/minisql/src/schema.rs @@ -1,4 +1,3 @@ -use crate::error::Error; use crate::internals::row::{ColumnPosition, Row}; use crate::operation::{InsertionValues, ColumnSelection}; use crate::result::DbResult; @@ -7,7 +6,7 @@ use bimap::BiMap; // 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, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct TableSchema { table_name: TableName, // used for descriptive errors primary_key: ColumnPosition, @@ -67,19 +66,15 @@ impl TableSchema { self.primary_key == column_position } - pub fn column_name_from_column_position( - &self, - column_position: ColumnPosition, - ) -> DbResult { + // Assumes `column` comes from a validated source. + pub fn column_name_from_column(&self, column: ColumnPosition) -> ColumnName { match self .column_name_position_mapping - .get_by_right(&column_position) + .get_by_right(&column) { - Some(column_name) => Ok(column_name.clone()), - None => Err(Error::ColumnPositionDoesNotExist( - self.table_name.clone(), - column_position, - )), + Some(column_name) => column_name.clone(), + None => unreachable!() // SAFETY: The only way this function can get a column is from + // validation, which guarantees there is such a colun. } } @@ -95,8 +90,8 @@ impl TableSchema { let id: Uuid = match row.get(self.primary_key) { Some(Value::Indexable(IndexableValue::Uuid(id))) => *id, - Some(_) => unreachable!(), // SAFETY: Should be guaranteed by validation - None => unreachable!(), // SAFETY: Should be guaranteed by validation + Some(_) => unreachable!(), // SAFETY: Should be guaranteed by validation (type-safety) + None => unreachable!(), // SAFETY: Should be guaranteed by validation (missing columns) }; Ok((id, row)) diff --git a/parser/src/core.rs b/parser/src/core.rs index 2cd2432..ec0c140 100644 --- a/parser/src/core.rs +++ b/parser/src/core.rs @@ -29,8 +29,8 @@ pub fn parse_statements<'a>(input: &'a str) -> IResult<&str, Vec many0(parse_statement)(input) } -pub fn parse_and_validate(query: String, db_schema: &DbSchema) -> Result { - let (_, op) = parse_statement(query.as_str()) +pub fn parse_and_validate(str_query: String, db_schema: &DbSchema) -> Result { + let (_, op) = parse_statement(str_query.as_str()) .map_err(|err| { Error::ParsingError(err.to_string()) })?; diff --git a/parser/src/validation.rs b/parser/src/validation.rs index 0f60f00..f76c9f0 100644 --- a/parser/src/validation.rs +++ b/parser/src/validation.rs @@ -1,5 +1,4 @@ -use std::collections::HashSet; -use std::collections::HashMap; +use std::collections::{HashSet, BTreeMap}; use thiserror::Error; use crate::syntax; @@ -28,8 +27,8 @@ pub enum ValidationError { } /// Validates and converts the raw syntax into a proper interpreter operation based on db schema. -pub fn validate_operation(query: RawQuerySyntax, db_schema: &DbSchema) -> Result { - match query { +pub fn validate_operation(syntax: RawQuerySyntax, db_schema: &DbSchema) -> Result { + match syntax { RawQuerySyntax::Select(table_name, column_selection, condition) => { validate_select(table_name, column_selection, condition, db_schema) }, @@ -76,9 +75,9 @@ pub fn validate_select(table_name: TableName, column_selection: syntax::ColumnSe let non_existant_columns: Vec = columns.iter().filter_map(|column| if schema.does_column_exist(&column) { - Some(column.clone()) - } else { None + } else { + Some(column.clone()) }).collect(); if non_existant_columns.len() > 0 { Err(ValidationError::ColumnsDoNotExist(non_existant_columns)) @@ -120,7 +119,10 @@ pub fn validate_insert(table_name: TableName, insertion_values: syntax::Insertio } // Check types and prepare for creation of InsertionValues for the interpreter - let mut values_map: HashMap<_, Value> = HashMap::new(); + let mut values_map: BTreeMap<_, Value> = BTreeMap::new(); // The reason for using BTreeMap + // instead of HashMap is that we need + // to get the values in a vector + // sorted by the key. for (column_name, value) in insertion_values { let (column, expected_type) = schema.get_column(&column_name).ok_or(ValidationError::ColumnsDoNotExist(vec![column_name.to_string()]))?; // By the previous validation steps this is never gonna trigger an error. let value_type = value.to_type(); @@ -130,9 +132,10 @@ pub fn validate_insert(table_name: TableName, insertion_values: syntax::Insertio values_map.insert(column, value); } - // These are values ordered by the column position - let values: operation::InsertionValues = values_map.into_values().collect(); + // WARNING: If you use `values_map: HashMap<_,_>`, this is not gonna sort values by key. + let values: operation::InsertionValues = values_map.into_values().collect(); + // Note that one of the values is id. Ok(Operation::Insert(table_position, values)) } @@ -191,3 +194,298 @@ fn get_table_schema<'a>(db_schema: &DbSchema<'a>, table_name: &'a TableName) -> let (_, _, table_schema) = db_schema.iter().find(|(tname, _, _)| table_name.eq(tname))?; Some(table_schema) } + +#[cfg(test)] +mod tests { + use crate::syntax::{RawQuerySyntax, ColumnSelection, Condition}; + use minisql::type_system::{Value, IndexableValue}; + use minisql::operation::Operation; + use minisql::operation; + use minisql::schema::TableSchema; + use super::*; + + use RawQuerySyntax::*; + use Value::*; + use IndexableValue::*; + use Condition::*; + + fn users_schema() -> TableSchema { + let id = 0; + let name = 1; + let age = 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], + ) + } + + fn db_schema(users_schema: &TableSchema) -> DbSchema { + vec![ + ("users".to_string(), 0, users_schema), + ] + } + + fn empty_db_schema() -> DbSchema<'static> { + vec![] + } + + // ====CreateTable==== + #[test] + fn test_create_basic() { + let users_schema: TableSchema = users_schema(); + let db_schema: DbSchema = empty_db_schema(); + + let syntax: RawQuerySyntax = CreateTable("users".to_string(), users_schema.clone()); + let result = validate_operation(syntax, &db_schema); + assert!(matches!(result, Ok(Operation::CreateTable(_, _)))); + + let Ok(Operation::CreateTable(table_name, _)) = result else { panic!() }; + assert!(table_name == "users".to_string()); + } + + // #[test] + // fn test_create_duplicates_in_schema() { + // let id = 0; + // let name = 1; + + // let users_schema = TableSchema::new( + // "users".to_string(), + // id, + // vec!( + // ("id".to_string(), id), + // ("name".to_string(), name), + // ("name".to_string(), name + 1), + // ), + // vec![DbType::Uuid, DbType::String, DbType::Int], + // ); + + // let db_schema: DbSchema = empty_db_schema(); + + // let syntax: RawQuerySyntax = CreateTable("users".to_string(), users_schema.clone()); + // let result = validate_operation(syntax, &db_schema); + // println!("{:?}", result); + // assert!(matches!(result, Err(ValidationError::DuplicateColumn(_)))); + + // // TODO + // } + + #[test] + fn test_create_already_exists() { + let users_schema: TableSchema = users_schema(); + let db_schema: DbSchema = db_schema(&users_schema); + + let syntax: RawQuerySyntax = CreateTable("users".to_string(), users_schema.clone()); + let result = validate_operation(syntax, &db_schema); + assert!(matches!(result, Err(ValidationError::TableAlreadyExists(_)))); + } + + // ====Select==== + #[test] + fn test_select_basic() { + let users_schema: TableSchema = users_schema(); + let db_schema: DbSchema = db_schema(&users_schema); + let users_position = 0; + let id = 0; + let name = 1; + let age = 2; + + let syntax: RawQuerySyntax = Select("users".to_string(), ColumnSelection::All, None); + let result = validate_operation(syntax, &db_schema); + assert!(matches!(result, Ok(Operation::Select(_, _, _)))); + + let Ok(Operation::Select(table_position, column_selection, condition)) = result else { panic!() }; + + assert!(table_position == users_position); + assert!(condition == None); + assert!(column_selection == vec![id, name, age]); + } + + #[test] + fn test_select_non_existent_table() { + let users_schema: TableSchema = users_schema(); + let db_schema: DbSchema = db_schema(&users_schema); + + let syntax: RawQuerySyntax = Select("does_not_exist".to_string(), ColumnSelection::All, None); + let result = validate_operation(syntax, &db_schema); + assert!(matches!(result, Err(ValidationError::TableDoesNotExist(_)))); + } + + #[test] + fn test_select_eq() { + let users_schema: TableSchema = users_schema(); + let db_schema: DbSchema = db_schema(&users_schema); + + let users_position = 0; + let id = 0; + let name = 1; + let age = 2; + + let syntax: RawQuerySyntax = Select("users".to_string(), ColumnSelection::All, Some(Eq("age".to_string(), Indexable(Int(25))))); + let result = validate_operation(syntax, &db_schema); + assert!(matches!(result, Ok(Operation::Select(_, _, _)))); + + let Ok(Operation::Select(table_position, column_selection, condition)) = result else { panic!() }; + + assert!(table_position == users_position); + assert!(column_selection == vec![id, name, age]); + + assert!(condition == Some(operation::Condition::Eq(age, Indexable(Int(25))))); + } + + #[test] + fn test_select_eq_columns_selection() { + let users_schema: TableSchema = users_schema(); + let db_schema: DbSchema = db_schema(&users_schema); + + let users_position = 0; + let name = 1; + let age = 2; + + let syntax: RawQuerySyntax = Select("users".to_string(), ColumnSelection::Columns(vec!["age".to_string(), "name".to_string(), "age".to_string()]), None); + let result = validate_operation(syntax, &db_schema); + assert!(matches!(result, Ok(Operation::Select(_, _, _)))); + + let Ok(Operation::Select(table_position, column_selection, condition)) = result else { panic!() }; + + assert!(table_position == users_position); + assert!(column_selection == vec![age, name, age]); + assert!(condition == None); + } + + #[test] + fn test_select_eq_columns_selection_nonexistent_column_selected() { + let users_schema: TableSchema = users_schema(); + let db_schema: DbSchema = db_schema(&users_schema); + + let syntax: RawQuerySyntax = Select("users".to_string(), ColumnSelection::Columns(vec!["age".to_string(), "does_not_exist".to_string()]), None); + let result = validate_operation(syntax, &db_schema); + assert!(matches!(result, Err(ValidationError::ColumnsDoNotExist(_)))); + } + + #[test] + fn test_select_eq_non_existent_column() { + let users_schema: TableSchema = users_schema(); + let db_schema: DbSchema = db_schema(&users_schema); + + let syntax: RawQuerySyntax = Select("users".to_string(), ColumnSelection::All, Some(Eq("does_not_exist".to_string(), Indexable(Int(25))))); + let result = validate_operation(syntax, &db_schema); + assert!(matches!(result, Err(ValidationError::ColumnsDoNotExist(_)))); + } + + #[test] + fn test_select_eq_type_error() { + let users_schema: TableSchema = users_schema(); + let db_schema: DbSchema = db_schema(&users_schema); + + let syntax: RawQuerySyntax = Select("users".to_string(), ColumnSelection::All, Some(Eq("age".to_string(), Indexable(String("25".to_string()))))); + let result = validate_operation(syntax, &db_schema); + assert!(matches!(result, Err(ValidationError::TypeMismatch { .. }))); + } + + // ====Insert==== + #[test] + fn test_insert() { + let users_schema: TableSchema = users_schema(); + let db_schema: DbSchema = db_schema(&users_schema); + + let users_position = 0; + + let syntax: RawQuerySyntax = Insert( + "users".to_string(), + vec![ + ("name".to_string(), Indexable(String("Alice".to_string()))), + ("id".to_string(), Indexable(Uuid(0))), + ("age".to_string(), Indexable(Int(25))), + ]); + let result = validate_operation(syntax, &db_schema); + assert!(matches!(result, Ok(Operation::Insert(_, _)))); + + let Ok(Operation::Insert(table_position, values)) = result else { panic!() }; + + assert!(table_position == users_position); + // Recall the order is + // let id = 0; + // let name = 1; + // let age = 2; + assert!(values == vec![Indexable(Uuid(0)), Indexable(String("Alice".to_string())), Indexable(Int(25))]); + } + + #[test] + fn test_insert_non_existent_column() { + let users_schema: TableSchema = users_schema(); + let db_schema: DbSchema = db_schema(&users_schema); + + let syntax: RawQuerySyntax = Insert( + "users".to_string(), + vec![ + ("name".to_string(), Indexable(String("Alice".to_string()))), + ("id".to_string(), Indexable(Uuid(0))), + ("age".to_string(), Indexable(Int(25))), + ("does_not_exist".to_string(), Indexable(Int(25))), + ]); + let result = validate_operation(syntax, &db_schema); + assert!(matches!(result, Err(ValidationError::ColumnsDoNotExist(_)))); + } + + #[test] + fn test_insert_ill_typed_column() { + let users_schema: TableSchema = users_schema(); + let db_schema: DbSchema = db_schema(&users_schema); + + let syntax: RawQuerySyntax = Insert( + "users".to_string(), + vec![ + ("name".to_string(), Indexable(String("Alice".to_string()))), + ("id".to_string(), Indexable(Uuid(0))), + ("age".to_string(), Number(25.0)), + ]); + let result = validate_operation(syntax, &db_schema); + assert!(matches!(result, Err(ValidationError::TypeMismatch { .. }))); + } + + // ====Delete==== + #[test] + fn test_delete_all() { + let users_schema: TableSchema = users_schema(); + let db_schema: DbSchema = db_schema(&users_schema); + + let users_position = 0; + + let syntax: RawQuerySyntax = Delete("users".to_string(), None); + let result = validate_operation(syntax, &db_schema); + assert!(matches!(result, Ok(Operation::Delete(_, None)))); + + let Ok(Operation::Delete(table_position, _)) = result else { panic!() }; + + assert!(table_position == users_position); + } + + #[test] + fn test_delete_eq() { + let users_schema: TableSchema = users_schema(); + let db_schema: DbSchema = db_schema(&users_schema); + + let users_position = 0; + let age = 2; + + let syntax: RawQuerySyntax = Delete("users".to_string(), Some(Eq("age".to_string(), Indexable(Int(25))))); + let result = validate_operation(syntax, &db_schema); + assert!(matches!(result, Ok(Operation::Delete(_, Some(operation::Condition::Eq(_, _)))))); + + let Ok(Operation::Delete(table_position, Some(operation::Condition::Eq(column_position, value)))) = result else { panic!() }; + + assert!(table_position == users_position); + assert!(column_position == age); + assert!(value == Indexable(Int(25))); + // assert!(condition == None); + } + + // ====CreateIndex==== +} diff --git a/server/src/proto_wrapper.rs b/server/src/proto_wrapper.rs index 3415255..bd74ba6 100644 --- a/server/src/proto_wrapper.rs +++ b/server/src/proto_wrapper.rs @@ -58,7 +58,7 @@ impl ServerProto for W where W: BackendProtoWriter + Send { async fn write_table_header(&mut self, table_schema: &TableSchema, row: &RestrictedRow) -> anyhow::Result<()> { let columns = row.iter() - .map(|(index, value)| value_to_column_description(table_schema, value, index)) + .map(|(index, value)| value_to_column_description(table_schema, value, *index)) .collect::>>()?; self.write_proto(RowDescriptionData { columns: columns.into() }.into()).await?; @@ -84,11 +84,11 @@ impl ServerProto for W where W: BackendProtoWriter + Send { } } -fn value_to_column_description(schema: &TableSchema, value: &Value, index: &usize) -> anyhow::Result { - let name = schema.column_name_from_column_position(*index)?; +fn value_to_column_description(schema: &TableSchema, value: &Value, index: usize) -> anyhow::Result { + let name = schema.column_name_from_column(index); let table_oid = schema.table_name().as_bytes().as_ptr() as i32; - let column_index = (*index).try_into()?; + let column_index = index.try_into()?; let type_oid = value.type_oid(); let type_size = value.type_size();