Add some tests for Validation

This commit is contained in:
Yuriy Dupyn 2024-01-28 15:09:27 +01:00
parent 10ba1dd3e4
commit 052236d892
8 changed files with 343 additions and 77 deletions

View file

@ -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}")]

View file

@ -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)
}

View file

@ -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<Condition>),
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<Value>;
pub type ColumnSelection = Vec<ColumnPosition>;
#[derive(Debug)]
#[derive(Debug, PartialEq)]
pub enum Condition {
Eq(ColumnPosition, Value),
}

View file

@ -1,3 +1,3 @@
use crate::error::Error;
use crate::error::RuntimeError;
pub type DbResult<A> = Result<A, Error>;
pub type DbResult<A> = Result<A, RuntimeError>;

View file

@ -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<ColumnName> {
// 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))

View file

@ -29,8 +29,8 @@ pub fn parse_statements<'a>(input: &'a str) -> IResult<&str, Vec<RawQuerySyntax>
many0(parse_statement)(input)
}
pub fn parse_and_validate(query: String, db_schema: &DbSchema) -> Result<Operation, Error> {
let (_, op) = parse_statement(query.as_str())
pub fn parse_and_validate(str_query: String, db_schema: &DbSchema) -> Result<Operation, Error> {
let (_, op) = parse_statement(str_query.as_str())
.map_err(|err| {
Error::ParsingError(err.to_string())
})?;

View file

@ -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<Operation, ValidationError> {
match query {
pub fn validate_operation(syntax: RawQuerySyntax, db_schema: &DbSchema) -> Result<Operation, ValidationError> {
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<ColumnName> =
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====
}

View file

@ -58,7 +58,7 @@ impl<W> 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::<anyhow::Result<Vec<ColumnDescription>>>()?;
self.write_proto(RowDescriptionData { columns: columns.into() }.into()).await?;
@ -84,11 +84,11 @@ impl<W> ServerProto for W where W: BackendProtoWriter + Send {
}
}
fn value_to_column_description(schema: &TableSchema, value: &Value, index: &usize) -> anyhow::Result<ColumnDescription> {
let name = schema.column_name_from_column_position(*index)?;
fn value_to_column_description(schema: &TableSchema, value: &Value, index: usize) -> anyhow::Result<ColumnDescription> {
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();