Merge branch 'interpreter' into 'main'
Interpreter See merge request x433485/minisql!2
This commit is contained in:
commit
71d4bd76f0
13 changed files with 1277 additions and 256 deletions
9
Cargo.lock
generated
9
Cargo.lock
generated
|
|
@ -55,6 +55,12 @@ dependencies = [
|
||||||
"rustc-demangle",
|
"rustc-demangle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bimap"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bincode"
|
name = "bincode"
|
||||||
version = "2.0.0-rc.3"
|
version = "2.0.0-rc.3"
|
||||||
|
|
@ -147,6 +153,9 @@ checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minisql"
|
name = "minisql"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"bimap",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
|
|
|
||||||
|
|
@ -6,3 +6,4 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
bimap = "0.6.3"
|
||||||
|
|
|
||||||
17
minisql/src/error.rs
Normal file
17
minisql/src/error.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
use crate::internals::row::ColumnPosition;
|
||||||
|
use crate::internals::schema::{ColumnName, TableName};
|
||||||
|
use crate::operation::InsertionValues;
|
||||||
|
use crate::type_system::{DbType, Uuid, Value};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
TableDoesNotExist(TableName),
|
||||||
|
ColumnDoesNotExist(TableName, ColumnName),
|
||||||
|
ColumnPositionDoesNotExist(TableName, ColumnPosition),
|
||||||
|
ValueDoesNotMatchExpectedType(TableName, ColumnName, DbType, Value),
|
||||||
|
AttemptingToInsertAlreadyPresentId(TableName, Uuid),
|
||||||
|
MissingTypeAnnotationOfColumn(TableName, ColumnPosition),
|
||||||
|
MissingColumnInInsertValues(TableName, ColumnName, InsertionValues),
|
||||||
|
MismatchBetweenInsertValuesAndColumns(TableName, InsertionValues),
|
||||||
|
AttemptToIndexNonIndexableColumn(TableName, ColumnName),
|
||||||
|
}
|
||||||
38
minisql/src/internals/column_index.rs
Normal file
38
minisql/src/internals/column_index.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
use crate::type_system::{IndexableValue, Uuid};
|
||||||
|
use std::collections::{BTreeMap, HashSet};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ColumnIndex {
|
||||||
|
index: BTreeMap<IndexableValue, HashSet<Uuid>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColumnIndex {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let index = BTreeMap::new();
|
||||||
|
Self { index }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, value: &IndexableValue) -> Option<&HashSet<Uuid>> {
|
||||||
|
self.index.get(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&mut self, value: IndexableValue, id: Uuid) {
|
||||||
|
match self.index.get_mut(&value) {
|
||||||
|
Some(ids) => {
|
||||||
|
ids.insert(id);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.index.insert(value, HashSet::from([id]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, value: &IndexableValue, id_to_be_removed: Uuid) -> bool {
|
||||||
|
match self.index.get_mut(value) {
|
||||||
|
Some(ids) => {
|
||||||
|
ids.remove(&id_to_be_removed) // true if was present
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
minisql/src/internals/mod.rs
Normal file
4
minisql/src/internals/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod column_index;
|
||||||
|
pub mod row;
|
||||||
|
pub mod schema;
|
||||||
|
pub mod table;
|
||||||
71
minisql/src/internals/row.rs
Normal file
71
minisql/src/internals/row.rs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
use crate::type_system::Value;
|
||||||
|
use std::ops::{Index, IndexMut};
|
||||||
|
use std::slice::SliceIndex;
|
||||||
|
|
||||||
|
pub type ColumnPosition = usize;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Row(Vec<Value>);
|
||||||
|
|
||||||
|
impl<Idx> Index<Idx> for Row
|
||||||
|
where
|
||||||
|
Idx: SliceIndex<[Value]>,
|
||||||
|
{
|
||||||
|
type Output = Idx::Output;
|
||||||
|
|
||||||
|
fn index(&self, index: Idx) -> &Self::Output {
|
||||||
|
&self.0[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Idx> IndexMut<Idx> for Row
|
||||||
|
where
|
||||||
|
Idx: SliceIndex<[Value]>,
|
||||||
|
{
|
||||||
|
fn index_mut(&mut self, index: Idx) -> &mut Self::Output {
|
||||||
|
&mut self.0[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromIterator<Value> for Row {
|
||||||
|
fn from_iter<I: IntoIterator<Item = Value>>(iter: I) -> Self {
|
||||||
|
let mut v = vec![];
|
||||||
|
for x in iter {
|
||||||
|
v.push(x)
|
||||||
|
}
|
||||||
|
Row(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Row {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Row(vec![])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_number_of_columns(number_of_columns: usize) -> Self {
|
||||||
|
Row(Vec::with_capacity(number_of_columns))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&mut self, value: Value) {
|
||||||
|
self.0.push(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.0.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, column_position: ColumnPosition) -> Option<&Value> {
|
||||||
|
self.0.get(column_position)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restrict_columns(&self, columns: &Vec<ColumnPosition>) -> Row {
|
||||||
|
// If the index from `columns` is non-existant in `row`, it will just ignore it.
|
||||||
|
let mut subrow: Row = Row::new();
|
||||||
|
for column_position in columns {
|
||||||
|
if let Some(value) = self.get(*column_position) {
|
||||||
|
subrow.0.push(value.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subrow
|
||||||
|
}
|
||||||
|
}
|
||||||
162
minisql/src/internals/schema.rs
Normal file
162
minisql/src/internals/schema.rs
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::internals::row::{ColumnPosition, Row};
|
||||||
|
use crate::operation::{ColumnSelection, InsertionValues};
|
||||||
|
use crate::result::DbResult;
|
||||||
|
use crate::type_system::{DbType, IndexableValue, Uuid, Value};
|
||||||
|
use bimap::BiMap;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
// Note that it is nice to split metadata from the data because
|
||||||
|
// then you can give the metadata to the parser without giving it the data.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TableSchema {
|
||||||
|
table_name: TableName, // used for descriptive errors
|
||||||
|
pub primary_key: ColumnPosition,
|
||||||
|
pub column_name_position_mapping: BiMap<ColumnName, ColumnPosition>,
|
||||||
|
pub types: Vec<DbType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type TableName = String;
|
||||||
|
pub type ColumnName = String;
|
||||||
|
|
||||||
|
impl TableSchema {
|
||||||
|
pub fn new(table_name: TableName, primary_key: ColumnPosition, column_name_position_map: Vec<(ColumnName, ColumnPosition)>, types: Vec<DbType>) -> Self {
|
||||||
|
let mut column_name_position_mapping: BiMap<ColumnName, ColumnPosition> = BiMap::new();
|
||||||
|
for (column_name, column_position) in column_name_position_map {
|
||||||
|
column_name_position_mapping.insert(column_name, column_position);
|
||||||
|
}
|
||||||
|
Self { table_name, primary_key, column_name_position_mapping, types }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn table_name(&self) -> &TableName {
|
||||||
|
&self.table_name
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_column(&self, column_name: &ColumnName) -> DbResult<(DbType, ColumnPosition)> {
|
||||||
|
match self.column_name_position_mapping.get_by_left(column_name) {
|
||||||
|
Some(column_position) => match self.types.get(*column_position) {
|
||||||
|
Some(type_) => Ok((*type_, *column_position)),
|
||||||
|
None => Err(Error::MissingTypeAnnotationOfColumn(
|
||||||
|
self.table_name.clone(),
|
||||||
|
*column_position,
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
None => Err(Error::ColumnDoesNotExist(
|
||||||
|
self.table_name.clone(),
|
||||||
|
column_name.clone(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn column_position_from_column_name(
|
||||||
|
&self,
|
||||||
|
column_name: &ColumnName,
|
||||||
|
) -> DbResult<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()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn row_from_insertion_values(
|
||||||
|
&self,
|
||||||
|
insertion_values: InsertionValues,
|
||||||
|
) -> DbResult<(Uuid, Row)> {
|
||||||
|
// TODO: There should be proper validation of the insertion_values.
|
||||||
|
// And it shouldn't really be done here.
|
||||||
|
//
|
||||||
|
// In the below we don't check for duplicate column names
|
||||||
|
//
|
||||||
|
let number_of_columns = self.number_of_columns();
|
||||||
|
if number_of_columns != insertion_values.len() {
|
||||||
|
return Err(Error::MismatchBetweenInsertValuesAndColumns(
|
||||||
|
self.table_name.clone(),
|
||||||
|
insertion_values,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut row: Row = Row::with_number_of_columns(number_of_columns);
|
||||||
|
|
||||||
|
let mut values: HashMap<ColumnName, Value> = HashMap::new();
|
||||||
|
for (column_name, db_value) in &insertion_values {
|
||||||
|
values.insert(column_name.clone(), db_value.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
for column_position in 0..number_of_columns {
|
||||||
|
let column_name: ColumnName = self.column_name_from_column_position(column_position)?;
|
||||||
|
match values.get(&column_name) {
|
||||||
|
Some(db_value) => row.push(db_value.clone()),
|
||||||
|
None => {
|
||||||
|
return Err(Error::MissingColumnInInsertValues(
|
||||||
|
self.table_name.clone(),
|
||||||
|
column_name,
|
||||||
|
insertion_values,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let id: Uuid = match row.get(self.primary_key) {
|
||||||
|
Some(Value::Indexable(IndexableValue::Uuid(id))) => *id,
|
||||||
|
Some(_) => unreachable!(),
|
||||||
|
None => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((id, row))
|
||||||
|
}
|
||||||
|
}
|
||||||
262
minisql/src/internals/table.rs
Normal file
262
minisql/src/internals/table.rs
Normal file
|
|
@ -0,0 +1,262 @@
|
||||||
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::internals::column_index::ColumnIndex;
|
||||||
|
use crate::internals::row::{ColumnPosition, Row};
|
||||||
|
use crate::internals::schema::{ColumnName, TableSchema, TableName};
|
||||||
|
use crate::result::DbResult;
|
||||||
|
use crate::type_system::{IndexableValue, Uuid, Value};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Table {
|
||||||
|
schema: TableSchema,
|
||||||
|
rows: Rows, // TODO: Consider wrapping this in a lock. Also consider if we need to have the
|
||||||
|
// same lock for both rows and indexes
|
||||||
|
indexes: HashMap<ColumnPosition, ColumnIndex>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Rows = BTreeMap<Uuid, Row>;
|
||||||
|
|
||||||
|
impl Table {
|
||||||
|
pub fn new(table_schema: TableSchema) -> Self {
|
||||||
|
Self {
|
||||||
|
schema: table_schema,
|
||||||
|
rows: BTreeMap::new(),
|
||||||
|
indexes: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn schema(&self) -> &TableSchema {
|
||||||
|
&self.schema
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rows(&self) -> &Rows {
|
||||||
|
&self.rows
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn indexes(&self) -> &HashMap<ColumnPosition, ColumnIndex> {
|
||||||
|
&self.indexes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn table_name(&self) -> &TableName {
|
||||||
|
&self.schema.table_name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======Selection======
|
||||||
|
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: &Value) -> Vec<Row> {
|
||||||
|
// brute-force search
|
||||||
|
self.rows
|
||||||
|
.values()
|
||||||
|
.filter_map(|row| {
|
||||||
|
if row.get(column_position) == Some(value) {
|
||||||
|
Some(row.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_all_rows(&self, selected_column_positions: &Vec<ColumnPosition>) -> Vec<Row> {
|
||||||
|
self.rows
|
||||||
|
.values()
|
||||||
|
.map(|row| row.restrict_columns(selected_column_positions))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_rows_where_eq(
|
||||||
|
&self,
|
||||||
|
selected_column_positions: &Vec<ColumnPosition>,
|
||||||
|
column_position: ColumnPosition,
|
||||||
|
value: Value,
|
||||||
|
) -> DbResult<Vec<Row>> {
|
||||||
|
match value {
|
||||||
|
Value::Indexable(value) => match self.fetch_ids_from_index(column_position, &value)? {
|
||||||
|
Some(ids) => Ok(self
|
||||||
|
.get_rows_by_ids(ids)
|
||||||
|
.iter()
|
||||||
|
.map(|row| row.restrict_columns(selected_column_positions))
|
||||||
|
.collect()),
|
||||||
|
None => Ok(self
|
||||||
|
.get_rows_by_value(column_position, &Value::Indexable(value))
|
||||||
|
.iter()
|
||||||
|
.map(|row| row.restrict_columns(selected_column_positions))
|
||||||
|
.collect()),
|
||||||
|
},
|
||||||
|
_ => Ok(self
|
||||||
|
.get_rows_by_value(column_position, &value)
|
||||||
|
.iter()
|
||||||
|
.map(|row| row.restrict_columns(selected_column_positions))
|
||||||
|
.collect()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======Insertion======
|
||||||
|
pub fn insert_row_at(&mut self, id: Uuid, row: Row) -> DbResult<()> {
|
||||||
|
if self.rows.get(&id).is_some() {
|
||||||
|
return Err(Error::AttemptingToInsertAlreadyPresentId(
|
||||||
|
self.table_name().clone(),
|
||||||
|
id,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (column_position, column_index) in &mut self.indexes {
|
||||||
|
match row.get(*column_position) {
|
||||||
|
Some(Value::Indexable(val)) => column_index.add(val.clone(), id),
|
||||||
|
Some(_) => {}
|
||||||
|
None => {
|
||||||
|
return Err(Error::ColumnPositionDoesNotExist(
|
||||||
|
self.schema.table_name().clone(), // Note that I can't simply use self.table_name() here because of rust borrowing rules.
|
||||||
|
*column_position,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = self.rows.insert(id, row);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======Deletion======
|
||||||
|
fn delete_row_by_id(&mut self, id: Uuid) -> usize {
|
||||||
|
match self.rows.remove(&id) {
|
||||||
|
Some(row) => {
|
||||||
|
for (column_position, column_index) in &mut self.indexes {
|
||||||
|
if let Value::Indexable(value) = &row[*column_position] {
|
||||||
|
let _ = column_index.remove(value, id);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
1
|
||||||
|
}
|
||||||
|
None => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_rows_by_ids(&mut self, ids: HashSet<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: &Value) -> 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 delete_all_rows(&mut self) -> usize {
|
||||||
|
let number_of_rows = self.rows.len();
|
||||||
|
self.rows = BTreeMap::new();
|
||||||
|
self.indexes = HashMap::new();
|
||||||
|
number_of_rows
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_rows_where_eq(
|
||||||
|
&mut self,
|
||||||
|
column_position: ColumnPosition,
|
||||||
|
value: Value,
|
||||||
|
) -> DbResult<usize> {
|
||||||
|
match value {
|
||||||
|
Value::Indexable(value) => match self.fetch_ids_from_index(column_position, &value)? {
|
||||||
|
Some(ids) => Ok(self.delete_rows_by_ids(ids)),
|
||||||
|
None => Ok(self.delete_rows_by_value(column_position, &Value::Indexable(value))),
|
||||||
|
},
|
||||||
|
_ => Ok(self.delete_rows_by_value(column_position, &value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======Indexing======
|
||||||
|
pub fn attach_index(&mut self, column_position: ColumnPosition) -> DbResult<()> {
|
||||||
|
let mut column_index: ColumnIndex = ColumnIndex::new();
|
||||||
|
update_index_from_table(&mut column_index, self, column_position)?;
|
||||||
|
self.indexes.insert(column_position, column_index);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_ids_from_index(
|
||||||
|
&self,
|
||||||
|
column_position: ColumnPosition,
|
||||||
|
value: &IndexableValue,
|
||||||
|
) -> DbResult<Option<HashSet<Uuid>>> {
|
||||||
|
if self.schema.is_primary(column_position) {
|
||||||
|
match value {
|
||||||
|
IndexableValue::Uuid(id) => Ok(Some(HashSet::from([*id]))),
|
||||||
|
_ => {
|
||||||
|
let column_name: ColumnName = self
|
||||||
|
.schema
|
||||||
|
.column_name_from_column_position(column_position)?;
|
||||||
|
let type_ = self.schema.types[column_position];
|
||||||
|
Err(Error::ValueDoesNotMatchExpectedType(
|
||||||
|
self.table_name().clone(),
|
||||||
|
column_name,
|
||||||
|
type_,
|
||||||
|
Value::Indexable(value.clone()),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match self.indexes.get(&column_position) {
|
||||||
|
Some(index) => {
|
||||||
|
// Note that we are cloning the ids here! This can be very wasteful in some cases.
|
||||||
|
// It would be possible to just return a reference,
|
||||||
|
// but this seems fairly non-trivial.
|
||||||
|
let ids = index.get(value).cloned();
|
||||||
|
Ok(ids)
|
||||||
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be used in the case when an index is created after the table has existed for a
|
||||||
|
// while. In such a case you need to build the index from the already existing rows.
|
||||||
|
fn update_index_from_table(
|
||||||
|
column_index: &mut ColumnIndex,
|
||||||
|
table: &Table,
|
||||||
|
column_position: ColumnPosition,
|
||||||
|
) -> DbResult<()> {
|
||||||
|
for (id, row) in &table.rows {
|
||||||
|
let value = match row.get(column_position) {
|
||||||
|
Some(Value::Indexable(value)) => value.clone(),
|
||||||
|
Some(_) => {
|
||||||
|
let column_name: ColumnName = table
|
||||||
|
.schema
|
||||||
|
.column_name_from_column_position(column_position)?;
|
||||||
|
return Err(Error::AttemptToIndexNonIndexableColumn(
|
||||||
|
table.table_name().to_string(),
|
||||||
|
column_name,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
return Err(Error::ColumnPositionDoesNotExist(
|
||||||
|
table.table_name().to_string(),
|
||||||
|
column_position,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
column_index.add(value, *id)
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
621
minisql/src/interpreter.rs
Normal file
621
minisql/src/interpreter.rs
Normal file
|
|
@ -0,0 +1,621 @@
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::internals::row::{ColumnPosition, Row};
|
||||||
|
use crate::internals::schema::{TableName, TableSchema};
|
||||||
|
use crate::internals::table::Table;
|
||||||
|
use crate::operation::{ColumnSelection, Condition, Operation};
|
||||||
|
use crate::result::DbResult;
|
||||||
|
use crate::type_system::{DbType, IndexableValue, Value};
|
||||||
|
use bimap::BiMap;
|
||||||
|
|
||||||
|
// Use `TablePosition` as index
|
||||||
|
pub type Tables = Vec<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
|
||||||
|
pub 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)?;
|
||||||
|
|
||||||
|
let selected_column_positions: Vec<ColumnPosition> = table
|
||||||
|
.schema()
|
||||||
|
.column_positions_from_column_selection(&column_selection)?;
|
||||||
|
let selected_rows = match maybe_condition {
|
||||||
|
None => table.select_all_rows(&selected_column_positions),
|
||||||
|
|
||||||
|
Some(Condition::Eq(eq_column_name, value)) => {
|
||||||
|
let eq_column_position = table
|
||||||
|
.schema()
|
||||||
|
.column_position_from_column_name(&eq_column_name)?;
|
||||||
|
table.select_rows_where_eq(
|
||||||
|
&selected_column_positions,
|
||||||
|
eq_column_position,
|
||||||
|
value,
|
||||||
|
)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Response::Selected(selected_rows))
|
||||||
|
}
|
||||||
|
Insert(table_name, values) => {
|
||||||
|
let table: &mut Table = self.table_from_name_mut(&table_name)?;
|
||||||
|
|
||||||
|
let (id, row) = table.schema().row_from_insertion_values(values)?;
|
||||||
|
table.insert_row_at(id, row)?;
|
||||||
|
Ok(Response::Inserted)
|
||||||
|
}
|
||||||
|
Delete(table_name, maybe_condition) => {
|
||||||
|
let table: &mut Table = self.table_from_name_mut(&table_name)?;
|
||||||
|
|
||||||
|
let rows_affected = match maybe_condition {
|
||||||
|
None => table.delete_all_rows(),
|
||||||
|
Some(Condition::Eq(eq_column_name, value)) => {
|
||||||
|
let eq_column_position = table
|
||||||
|
.schema()
|
||||||
|
.column_position_from_column_name(&eq_column_name)?;
|
||||||
|
table.delete_rows_where_eq(eq_column_position, value)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Response::Deleted(rows_affected))
|
||||||
|
}
|
||||||
|
CreateTable(table_name, table_schema) => {
|
||||||
|
let table = Table::new(table_schema);
|
||||||
|
self.attach_table(table_name, table);
|
||||||
|
|
||||||
|
Ok(Response::TableCreated)
|
||||||
|
}
|
||||||
|
CreateIndex(table_name, column_name) => {
|
||||||
|
let table: &mut Table = self.table_from_name_mut(&table_name)?;
|
||||||
|
let column_position: ColumnPosition = table
|
||||||
|
.schema()
|
||||||
|
.column_position_from_column_name(&column_name)?;
|
||||||
|
|
||||||
|
table.attach_index(column_position)?;
|
||||||
|
Ok(Response::IndexCreated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Give a better name to something that you can respond to with rows
|
||||||
|
trait SqlResponseConsumer {
|
||||||
|
// TODO:
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
fn users_schema() -> TableSchema {
|
||||||
|
let id: ColumnPosition = 0;
|
||||||
|
let name: ColumnPosition = 1;
|
||||||
|
let age: ColumnPosition = 2;
|
||||||
|
|
||||||
|
TableSchema::new(
|
||||||
|
"users".to_string(),
|
||||||
|
id,
|
||||||
|
vec!(
|
||||||
|
("id".to_string(), id),
|
||||||
|
("name".to_string(), name),
|
||||||
|
("age".to_string(), age),
|
||||||
|
),
|
||||||
|
vec![DbType::Uuid, DbType::String, DbType::Int],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_table_creation() {
|
||||||
|
let mut state = State::new();
|
||||||
|
let users_schema = users_schema();
|
||||||
|
let users = users_schema.table_name().clone();
|
||||||
|
|
||||||
|
state
|
||||||
|
.interpret(Operation::CreateTable(users.clone(), users_schema))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(state.tables.len() == 1);
|
||||||
|
let table = &state.tables[0];
|
||||||
|
assert!(table.rows().len() == 0);
|
||||||
|
|
||||||
|
assert!(table.table_name() == &users);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_select_empty() {
|
||||||
|
let mut state = State::new();
|
||||||
|
let users_schema = users_schema();
|
||||||
|
let users = users_schema.table_name().clone();
|
||||||
|
|
||||||
|
state
|
||||||
|
.interpret(Operation::CreateTable(users.clone(), users_schema))
|
||||||
|
.unwrap();
|
||||||
|
let response: Response = state
|
||||||
|
.interpret(Operation::Select(users.clone(), ColumnSelection::All, None))
|
||||||
|
.unwrap();
|
||||||
|
assert!(matches!(response, Response::Selected(_)));
|
||||||
|
let Response::Selected(rows) = response else {
|
||||||
|
todo!()
|
||||||
|
};
|
||||||
|
assert!(rows.len() == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_select_nonexistant_table() {
|
||||||
|
let mut state = State::new();
|
||||||
|
|
||||||
|
let response: DbResult<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 IndexableValue::*;
|
||||||
|
use Value::*;
|
||||||
|
|
||||||
|
let mut state = State::new();
|
||||||
|
let users_schema = users_schema();
|
||||||
|
let users = users_schema.table_name().clone();
|
||||||
|
|
||||||
|
state
|
||||||
|
.interpret(Operation::CreateTable(users.clone(), users_schema))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (id, name, age) = (
|
||||||
|
Indexable(Uuid(0)),
|
||||||
|
Indexable(String("Plato".to_string())),
|
||||||
|
Indexable(Int(64)),
|
||||||
|
);
|
||||||
|
state
|
||||||
|
.interpret(Operation::Insert(
|
||||||
|
users.clone(),
|
||||||
|
vec![
|
||||||
|
("id".to_string(), id.clone()),
|
||||||
|
("name".to_string(), name.clone()),
|
||||||
|
("age".to_string(), age.clone()),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response: Response = state
|
||||||
|
.interpret(Operation::Select(users.clone(), ColumnSelection::All, None))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(response, Response::Selected(_)));
|
||||||
|
let Response::Selected(rows) = response else {
|
||||||
|
todo!()
|
||||||
|
};
|
||||||
|
assert!(rows.len() == 1);
|
||||||
|
let row = &rows[0];
|
||||||
|
|
||||||
|
assert!(row.len() == 3);
|
||||||
|
assert!(row[0] == id);
|
||||||
|
assert!(row[1] == name);
|
||||||
|
assert!(row[2] == age);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_insert_select_basic2() {
|
||||||
|
use ColumnSelection::*;
|
||||||
|
use Condition::*;
|
||||||
|
use IndexableValue::*;
|
||||||
|
use Operation::*;
|
||||||
|
use Value::*;
|
||||||
|
|
||||||
|
let mut state = State::new();
|
||||||
|
let users_schema = users_schema();
|
||||||
|
let users = users_schema.table_name().clone();
|
||||||
|
|
||||||
|
state
|
||||||
|
.interpret(CreateTable(users.clone(), users_schema))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (id0, name0, age0) = (
|
||||||
|
Indexable(Uuid(0)),
|
||||||
|
Indexable(String("Plato".to_string())),
|
||||||
|
Indexable(Int(64)),
|
||||||
|
);
|
||||||
|
state
|
||||||
|
.interpret(Insert(
|
||||||
|
users.clone(),
|
||||||
|
vec![
|
||||||
|
("id".to_string(), id0.clone()),
|
||||||
|
("name".to_string(), name0.clone()),
|
||||||
|
("age".to_string(), age0.clone()),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (id1, name1, age1) = (
|
||||||
|
Indexable(Uuid(1)),
|
||||||
|
Indexable(String("Aristotle".to_string())),
|
||||||
|
Indexable(Int(20)),
|
||||||
|
);
|
||||||
|
state
|
||||||
|
.interpret(Insert(
|
||||||
|
users.clone(),
|
||||||
|
vec![
|
||||||
|
("id".to_string(), id1.clone()),
|
||||||
|
("name".to_string(), name1.clone()),
|
||||||
|
("age".to_string(), age1.clone()),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
let response: Response = state.interpret(Select(users.clone(), All, None)).unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(response, Response::Selected(_)));
|
||||||
|
let Response::Selected(rows) = response else {
|
||||||
|
todo!()
|
||||||
|
};
|
||||||
|
assert!(rows.len() == 2);
|
||||||
|
let row0 = &rows[0];
|
||||||
|
let row1 = &rows[1];
|
||||||
|
|
||||||
|
assert!(row0.len() == 3);
|
||||||
|
assert!(row0[0] == id0);
|
||||||
|
assert!(row0[1] == name0);
|
||||||
|
assert!(row0[2] == age0);
|
||||||
|
|
||||||
|
assert!(row1.len() == 3);
|
||||||
|
assert!(row1[0] == id1);
|
||||||
|
assert!(row1[1] == name1);
|
||||||
|
assert!(row1[2] == age1);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let response: Response = state
|
||||||
|
.interpret(Select(
|
||||||
|
users.clone(),
|
||||||
|
All,
|
||||||
|
Some(Eq("id".to_string(), id0.clone())),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
assert!(matches!(response, Response::Selected(_)));
|
||||||
|
let Response::Selected(rows) = response else {
|
||||||
|
todo!()
|
||||||
|
};
|
||||||
|
assert!(rows.len() == 1);
|
||||||
|
let row0 = &rows[0];
|
||||||
|
|
||||||
|
assert!(row0.len() == 3);
|
||||||
|
assert!(row0[0] == id0);
|
||||||
|
assert!(row0[1] == name0);
|
||||||
|
assert!(row0[2] == age0);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let response: Response = state
|
||||||
|
.interpret(Select(
|
||||||
|
users.clone(),
|
||||||
|
Columns(vec!["name".to_string(), "id".to_string()]),
|
||||||
|
Some(Eq("id".to_string(), id0.clone())),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
assert!(matches!(response, Response::Selected(_)));
|
||||||
|
let Response::Selected(rows) = response else {
|
||||||
|
todo!()
|
||||||
|
};
|
||||||
|
assert!(rows.len() == 1);
|
||||||
|
let row0 = &rows[0];
|
||||||
|
|
||||||
|
assert!(row0.len() == 2);
|
||||||
|
assert!(row0[0] == name0);
|
||||||
|
assert!(row0[1] == id0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete() {
|
||||||
|
use ColumnSelection::*;
|
||||||
|
use Condition::*;
|
||||||
|
use IndexableValue::*;
|
||||||
|
use Operation::*;
|
||||||
|
use Value::*;
|
||||||
|
|
||||||
|
let mut state = State::new();
|
||||||
|
let users_schema = users_schema();
|
||||||
|
let users = users_schema.table_name().clone();
|
||||||
|
|
||||||
|
state
|
||||||
|
.interpret(CreateTable(users.clone(), users_schema))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (id0, name0, age0) = (
|
||||||
|
Indexable(Uuid(0)),
|
||||||
|
Indexable(String("Plato".to_string())),
|
||||||
|
Indexable(Int(64)),
|
||||||
|
);
|
||||||
|
state
|
||||||
|
.interpret(Insert(
|
||||||
|
users.clone(),
|
||||||
|
vec![
|
||||||
|
("id".to_string(), id0.clone()),
|
||||||
|
("name".to_string(), name0.clone()),
|
||||||
|
("age".to_string(), age0.clone()),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (id1, name1, age1) = (
|
||||||
|
Indexable(Uuid(1)),
|
||||||
|
Indexable(String("Aristotle".to_string())),
|
||||||
|
Indexable(Int(20)),
|
||||||
|
);
|
||||||
|
state
|
||||||
|
.interpret(Insert(
|
||||||
|
users.clone(),
|
||||||
|
vec![
|
||||||
|
("id".to_string(), id1.clone()),
|
||||||
|
("name".to_string(), name1.clone()),
|
||||||
|
("age".to_string(), age1.clone()),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let delete_response: Response = state
|
||||||
|
.interpret(Delete(
|
||||||
|
users.clone(),
|
||||||
|
Some(Eq("id".to_string(), id0.clone())),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
assert!(matches!(delete_response, Response::Deleted(1)));
|
||||||
|
|
||||||
|
let response: Response = state.interpret(Select(users.clone(), All, None)).unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(response, Response::Selected(_)));
|
||||||
|
let Response::Selected(rows) = response else {
|
||||||
|
todo!()
|
||||||
|
};
|
||||||
|
assert!(rows.len() == 1);
|
||||||
|
let row = &rows[0];
|
||||||
|
|
||||||
|
assert!(row.len() == 3);
|
||||||
|
assert!(row[0] == id1);
|
||||||
|
assert!(row[1] == name1);
|
||||||
|
assert!(row[2] == age1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_index() {
|
||||||
|
use IndexableValue::*;
|
||||||
|
use Operation::*;
|
||||||
|
use Value::*;
|
||||||
|
|
||||||
|
let mut state = State::new();
|
||||||
|
let users_schema = users_schema();
|
||||||
|
let users = users_schema.table_name().clone();
|
||||||
|
|
||||||
|
state
|
||||||
|
.interpret(CreateTable(users.clone(), users_schema))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
state
|
||||||
|
.interpret(CreateIndex(users.clone(), "name".to_string()))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (id0, name0, age0) = (
|
||||||
|
Indexable(Uuid(0)),
|
||||||
|
Indexable(String("Plato".to_string())),
|
||||||
|
Indexable(Int(64)),
|
||||||
|
);
|
||||||
|
state
|
||||||
|
.interpret(Insert(
|
||||||
|
users.clone(),
|
||||||
|
vec![
|
||||||
|
("id".to_string(), id0.clone()),
|
||||||
|
("name".to_string(), name0.clone()),
|
||||||
|
("age".to_string(), age0.clone()),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (id1, name1, age1) = (
|
||||||
|
Indexable(Uuid(1)),
|
||||||
|
Indexable(String("Aristotle".to_string())),
|
||||||
|
Indexable(Int(20)),
|
||||||
|
);
|
||||||
|
state
|
||||||
|
.interpret(Insert(
|
||||||
|
users.clone(),
|
||||||
|
vec![
|
||||||
|
("id".to_string(), id1.clone()),
|
||||||
|
("name".to_string(), name1.clone()),
|
||||||
|
("age".to_string(), age1.clone()),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(state.tables.len() == 1);
|
||||||
|
let table = &state.tables[0];
|
||||||
|
assert!(table.rows().len() == 2);
|
||||||
|
|
||||||
|
let user: ColumnPosition = 1;
|
||||||
|
assert!(table.indexes().contains_key(&user));
|
||||||
|
|
||||||
|
let index = table.indexes().get(&user).unwrap();
|
||||||
|
|
||||||
|
let plato_id = 0;
|
||||||
|
let aristotle_id = 1;
|
||||||
|
|
||||||
|
let plato_ids = index.get(&String("Plato".to_string())).cloned().unwrap_or(HashSet::new());
|
||||||
|
assert!(plato_ids.contains(&plato_id));
|
||||||
|
assert!(!plato_ids.contains(&aristotle_id));
|
||||||
|
assert!(plato_ids.len() == 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn example() {
|
||||||
|
use ColumnSelection::*;
|
||||||
|
use Condition::*;
|
||||||
|
use IndexableValue::*;
|
||||||
|
use Operation::*;
|
||||||
|
use Value::*;
|
||||||
|
|
||||||
|
let users_schema: TableSchema = {
|
||||||
|
let id: ColumnPosition = 0;
|
||||||
|
let name: ColumnPosition = 1;
|
||||||
|
let age: ColumnPosition = 2;
|
||||||
|
|
||||||
|
TableSchema::new(
|
||||||
|
"users".to_string(),
|
||||||
|
id,
|
||||||
|
vec!(
|
||||||
|
("id".to_string(), id),
|
||||||
|
("name".to_string(), name),
|
||||||
|
("age".to_string(), age),
|
||||||
|
),
|
||||||
|
vec![DbType::Uuid, DbType::String, DbType::Int],
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let users = users_schema.table_name().clone();
|
||||||
|
|
||||||
|
let mut state = State::new();
|
||||||
|
state
|
||||||
|
.interpret(Operation::CreateTable(users.clone(), users_schema))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (id0, name0, age0) = (
|
||||||
|
Indexable(Uuid(0)),
|
||||||
|
Indexable(String("Plato".to_string())),
|
||||||
|
Indexable(Int(64)),
|
||||||
|
);
|
||||||
|
println!("==INSERT Plato==");
|
||||||
|
state
|
||||||
|
.interpret(Insert(
|
||||||
|
users.clone(),
|
||||||
|
vec![
|
||||||
|
("id".to_string(), id0.clone()),
|
||||||
|
("name".to_string(), name0.clone()),
|
||||||
|
("age".to_string(), age0.clone()),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (id1, name1, age1) = (
|
||||||
|
Indexable(Uuid(1)),
|
||||||
|
Indexable(String("Aristotle".to_string())),
|
||||||
|
Indexable(Int(20)),
|
||||||
|
);
|
||||||
|
println!("==INSERT Aristotle==");
|
||||||
|
state
|
||||||
|
.interpret(Insert(
|
||||||
|
users.clone(),
|
||||||
|
vec![
|
||||||
|
("id".to_string(), id1.clone()),
|
||||||
|
("name".to_string(), name1.clone()),
|
||||||
|
("age".to_string(), age1.clone()),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
println!();
|
||||||
|
|
||||||
|
{
|
||||||
|
let response: Response = state
|
||||||
|
.interpret(Operation::Select(users.clone(), ColumnSelection::All, None))
|
||||||
|
.unwrap();
|
||||||
|
println!("==SELECT ALL==");
|
||||||
|
println!("{:?}", response);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let response: Response = state
|
||||||
|
.interpret(Select(
|
||||||
|
users.clone(),
|
||||||
|
All,
|
||||||
|
Some(Eq("id".to_string(), id0.clone())),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
println!("==SELECT Plato==");
|
||||||
|
println!("{:?}", response);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let _delete_response: Response = state
|
||||||
|
.interpret(Delete(
|
||||||
|
users.clone(),
|
||||||
|
Some(Eq("id".to_string(), id0.clone())),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
println!("==DELETE Plato==");
|
||||||
|
let response: Response = state
|
||||||
|
.interpret(Select(
|
||||||
|
users.clone(),
|
||||||
|
Columns(vec!["name".to_string(), "id".to_string()]),
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
println!("==SELECT All==");
|
||||||
|
println!("{:?}", response);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,259 +1,10 @@
|
||||||
use std::collections::{BTreeMap, HashMap};
|
mod error;
|
||||||
|
mod internals;
|
||||||
// ==============SQL operations================
|
mod interpreter;
|
||||||
// TODO: Note that every operation has a table name.
|
mod operation;
|
||||||
// Perhaps consider factoring the table name out
|
mod result;
|
||||||
// and think of the operations as operating on a unique table.
|
mod type_system;
|
||||||
enum Operation {
|
|
||||||
Select(TableName, ColumnSelection, Option<Condition>),
|
|
||||||
Insert(TableName, InsertionValues),
|
|
||||||
Delete(TableName, Option<Condition>),
|
|
||||||
// Update(...),
|
|
||||||
CreateTable(TableName, TableSchema),
|
|
||||||
CreateIndex(TableName, ColumnName), // TODO: Is this sufficient?
|
|
||||||
// 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 as in SQL, it sucks.
|
|
||||||
// I would rather have non-nullable values by default,
|
|
||||||
// and something like an explicit Option type for nulls.
|
|
||||||
enum DbValue {
|
|
||||||
String(String),
|
|
||||||
Int(u64),
|
|
||||||
Number(f64),
|
|
||||||
UUID(UUID),
|
|
||||||
// TODO: what bout null?
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Can this be autogenerated from the values?
|
|
||||||
enum DbType {
|
|
||||||
String,
|
|
||||||
Int,
|
|
||||||
Number,
|
|
||||||
UUID,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DbValue {
|
|
||||||
// TODO: Can this be autogenerated?
|
|
||||||
fn to_type(self) -> DbType {
|
|
||||||
match self {
|
|
||||||
Self::String(_) => DbType::String,
|
|
||||||
Self::Int(_) => DbType::Int,
|
|
||||||
Self::Number(_) => DbType::Number,
|
|
||||||
Self::UUID(_) => DbType::UUID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ==============Tables================
|
|
||||||
// table-metadata and data
|
|
||||||
|
|
||||||
type TableName = String;
|
|
||||||
type TablePosition = u32;
|
|
||||||
|
|
||||||
struct Table {
|
|
||||||
schema: TableSchema,
|
|
||||||
rows: Rows, // TODO: Consider wrapping this in a lock. Also consider if we need to have the
|
|
||||||
// same lock for both rows and indexes
|
|
||||||
indexes:
|
|
||||||
HashMap<ColumnPosition, ColumnIndex> // TODO: Consider generalizing `ColumnPosition` to something that would also apply to a pair of `ColumnNames` etc
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Is this really indexed by DbValues?
|
|
||||||
// Maybe we should have a separate index type for each type of value we're indexing over
|
|
||||||
struct ColumnIndex {
|
|
||||||
index: BTreeMap<DbValue, 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.
|
|
||||||
struct TableSchema {
|
|
||||||
columns: HashMap<ColumnName, (DbType, ColumnPosition)>
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
fn column_position(table_meta: TableSchema, column_name: ColumnName) -> Option<ColumnPosition> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use `TablePosition` as index
|
|
||||||
type Tables = Vec<Table>;
|
|
||||||
|
|
||||||
|
|
||||||
type ColumnName = String;
|
|
||||||
type ColumnPosition = u32;
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
|
|
||||||
// ==============Interpreter================
|
|
||||||
struct State {
|
|
||||||
table_positions: HashMap<TableName, TablePosition>,
|
|
||||||
tables: Vec<Table>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl State {
|
|
||||||
fn table_from_name<'b: 'a, 'a>(&'b self, table_name: TableName) -> Option<&'a Table> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn attach_table(&mut self, table: Table) {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Give a better name to something that you can respond to with rows
|
|
||||||
trait SqlConsumer {
|
|
||||||
// TODO:
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: This should return a reference to the table
|
|
||||||
// 'tables_life contains 'table_life
|
|
||||||
fn get_table<'tables_life: 'table_life, 'table_life>(tables: &'tables_life Tables, table_name: &TableName) -> &'table_life Table {
|
|
||||||
// let table_position:
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Decide if we want for this to return a response (but then you have to deal with lifetimes,
|
|
||||||
// because you'll be forced to put an iterator/slice into the Response data-structure.
|
|
||||||
// Alternative is to pass a row-consumer to the functionas that knows how to communicate with
|
|
||||||
// the client, but the details of communication are hidden behind an interface
|
|
||||||
fn interpret(table_name: TableName, operation: Operation, state: &mut State, consumer: impl SqlConsumer) -> () {
|
|
||||||
// TODO: lock stuff
|
|
||||||
use Operation::*;
|
|
||||||
|
|
||||||
match operation {
|
|
||||||
Select(table_name, column_selection, maybe_condition) => {
|
|
||||||
let table: &Table = todo!();
|
|
||||||
table.select_where(column_selection, maybe_condition, consumer)
|
|
||||||
},
|
|
||||||
Insert(table_name, values) => {
|
|
||||||
let table: &mut Table = todo!();
|
|
||||||
|
|
||||||
table.insert(values, consumer)
|
|
||||||
},
|
|
||||||
Delete(table_name, maybe_condition) => {
|
|
||||||
let table: &mut Table = todo!();
|
|
||||||
|
|
||||||
table.delete_where(maybe_condition, consumer)
|
|
||||||
},
|
|
||||||
CreateTable(table_name, table_schema) => {
|
|
||||||
let table = Table::new(table_name, table_schema);
|
|
||||||
state.attach_table(table);
|
|
||||||
todo!()
|
|
||||||
},
|
|
||||||
CreateIndex(table_name, column_name) => {
|
|
||||||
let table: &mut Table = todo!();
|
|
||||||
|
|
||||||
let index: ColumnIndex = ColumnIndex::new(table, column_name);
|
|
||||||
table.attach_index(index);
|
|
||||||
}, // TODO: Is this sufficient?
|
|
||||||
//
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ColumnIndex {
|
|
||||||
fn new(table: &Table, column_name: ColumnName) -> ColumnIndex {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
impl Table {
|
|
||||||
fn new(table_name: TableName, table_schema: TableSchema) -> Table {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn attach_index(&mut self, column_index: ColumnIndex) {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn select_where(&self, column_selection: ColumnSelection, maybe_condition: Option<Condition>, consumer: impl SqlConsumer) {
|
|
||||||
match maybe_condition {
|
|
||||||
None => {
|
|
||||||
// .iter() will give us an iterator over all the rows
|
|
||||||
|
|
||||||
// two choices
|
|
||||||
// 1. optimized version
|
|
||||||
// self.iter_with_columns(column_selection).for_each(|row| {
|
|
||||||
// consumer.send(row)
|
|
||||||
// });
|
|
||||||
// 2.
|
|
||||||
// self.iter()
|
|
||||||
// .map(|row| row.select_columns(column_selection))
|
|
||||||
// .for_each(|reduced_row| {
|
|
||||||
// consumer.send(row)
|
|
||||||
// });
|
|
||||||
todo!()
|
|
||||||
},
|
|
||||||
Some(Condition::Eq(column_name, value)) => {
|
|
||||||
// is column_name primary key? then it is easy
|
|
||||||
// self.get(id)
|
|
||||||
// is column_name indexed? Then get the index, and then it is not easy, because you
|
|
||||||
// may get a set of ids.
|
|
||||||
// what if it is not primary nor indexed? then you need to brute force your way
|
|
||||||
// through the whole table?
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert(&mut self, values: InsertionValues, consumer: impl SqlConsumer) {
|
|
||||||
// 1. You need to update indices
|
|
||||||
// 2. you simply insert the data
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_where(&mut self, maybe_condition: Option<Condition>, consumer: impl SqlConsumer) {
|
|
||||||
// kinda similar to select with respect to the conditions
|
|
||||||
// update index
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// enum Response {
|
|
||||||
// Selected(impl Iter<???>), // TODO: How to do this? Some reference to an iterator somehow... slice..?
|
|
||||||
// Inserted(???),
|
|
||||||
// Deleted(usize), // how many were deleted
|
|
||||||
// }
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("Hello, world!");
|
interpreter::example();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
40
minisql/src/operation.rs
Normal file
40
minisql/src/operation.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
use crate::internals::schema::{ColumnName, TableName, TableSchema};
|
||||||
|
use crate::type_system::Value;
|
||||||
|
|
||||||
|
// ==============SQL operations================
|
||||||
|
// TODO: Note that every operation has a table name.
|
||||||
|
// Perhaps consider factoring the table name out
|
||||||
|
// and think of the operations as operating on a unique table.
|
||||||
|
// TODO: `TableName` should be replaced by `TablePosition`
|
||||||
|
pub enum Operation {
|
||||||
|
Select(TableName, ColumnSelection, Option<Condition>),
|
||||||
|
Insert(TableName, InsertionValues),
|
||||||
|
Delete(TableName, Option<Condition>),
|
||||||
|
// Update(...),
|
||||||
|
CreateTable(TableName, TableSchema),
|
||||||
|
CreateIndex(TableName, ColumnName),
|
||||||
|
// DropTable(TableName),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type InsertionValues = Vec<(ColumnName, Value)>;
|
||||||
|
|
||||||
|
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, Value),
|
||||||
|
// LessOrEqual(ColumnName, DbValue),
|
||||||
|
// Less(ColumnName, DbValue),
|
||||||
|
|
||||||
|
// StringCondition(StringCondition),
|
||||||
|
}
|
||||||
|
|
||||||
|
// enum StringCondition {
|
||||||
|
// Prefix(ColumnName, String),
|
||||||
|
// Substring(ColumnName, String),
|
||||||
|
// }
|
||||||
3
minisql/src/result.rs
Normal file
3
minisql/src/result.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
pub type DbResult<A> = Result<A, Error>;
|
||||||
42
minisql/src/type_system.rs
Normal file
42
minisql/src/type_system.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
// ==============Types================
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum DbType {
|
||||||
|
String,
|
||||||
|
Int,
|
||||||
|
Number,
|
||||||
|
Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============Values================
|
||||||
|
pub type Uuid = u64;
|
||||||
|
|
||||||
|
// TODO: What about nulls? I would rather not have that in SQL, it sucks.
|
||||||
|
// I would rather have non-nullable values by default,
|
||||||
|
// and something like an explicit Option type for nulls.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Value {
|
||||||
|
Number(f64), // TODO: Can't put floats as keys in maps, since they don't implement Eq. What to
|
||||||
|
// do?
|
||||||
|
Indexable(IndexableValue),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Ord, Eq, Clone, PartialOrd, PartialEq)]
|
||||||
|
pub enum IndexableValue {
|
||||||
|
String(String),
|
||||||
|
Int(u64),
|
||||||
|
Uuid(Uuid),
|
||||||
|
// TODO: what about null?
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Value {
|
||||||
|
pub fn to_type(&self) -> DbType {
|
||||||
|
match self {
|
||||||
|
Self::Number(_) => DbType::Number,
|
||||||
|
Self::Indexable(val) => match val {
|
||||||
|
IndexableValue::String(_) => DbType::String,
|
||||||
|
IndexableValue::Int(_) => DbType::Int,
|
||||||
|
IndexableValue::Uuid(_) => DbType::Uuid,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue