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