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